1use std::sync::{Arc, OnceLock};
2
3use tokio::sync::mpsc::UnboundedSender;
4
5use crate::PluginMessage;
6use crate::tool_dispatch::ToolDispatchContext;
7use crate::{
8 PromptContribution, RuntimeServices, SandboxMessage, SessionStreamEvent, ToolProvider,
9};
10
11mod execution_context;
12pub(crate) mod process_handles;
13mod tool_execution;
14
15pub use execution_context::RuntimeExecutionContext;
16pub(crate) use execution_context::RuntimeExecutionTracing;
17pub use tool_execution::{ToolInvocation, ToolInvocationReply};
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20struct ToolCatalogCacheKey {
21 include_base_tools: bool,
22 context_overlay_revision: u64,
23 tool_generation: u64,
24 plugin_revision: u64,
25}
26
27#[derive(Debug, Default)]
28struct ToolCatalogDerived {
29 catalog: OnceLock<Arc<Vec<serde_json::Value>>>,
30}
31
32struct ToolCatalogArtifact {
33 tool_catalog: Arc<crate::ToolCatalog>,
34 preamble: Arc<crate::TurnDriverPreamble>,
35 derived: ToolCatalogDerived,
36}
37
38#[derive(Clone)]
39pub(crate) struct ToolCatalogHandle(Arc<ToolCatalogArtifact>);
40
41impl ToolCatalogHandle {
42 fn tool_catalog(&self) -> Arc<crate::ToolCatalog> {
43 Arc::clone(&self.0.tool_catalog)
44 }
45
46 fn preamble(&self) -> Arc<crate::TurnDriverPreamble> {
47 Arc::clone(&self.0.preamble)
48 }
49
50 fn catalog(&self) -> Arc<Vec<serde_json::Value>> {
51 Arc::clone(self.0.derived.catalog.get_or_init(|| {
52 Arc::new(crate::tool_registry::project_tool_catalog(
53 self.0.tool_catalog.tools.iter().cloned(),
54 ))
55 }))
56 }
57}
58
59#[derive(Clone, Debug)]
60pub struct InjectedTurnInput {
61 pub id: Option<String>,
62 pub message: PluginMessage,
63}
64
65#[derive(Debug, thiserror::Error)]
66pub enum SessionError {
67 #[error("I/O error: {0}")]
68 Io(#[from] std::io::Error),
69 #[error("JSON error: {0}")]
70 Json(#[from] serde_json::Error),
71 #[error("code execution is not available in this session")]
72 CodeExecutionUnavailable,
73 #[error("code execution runtime exited unexpectedly")]
74 CodeExecutionRuntimeStopped,
75 #[error(
76 "provider mismatch for session `{session_id}`: persisted provider `{expected}` does not match live provider `{actual}`"
77 )]
78 ProviderMismatch {
79 expected: String,
80 actual: String,
81 session_id: String,
82 },
83 #[error("provider is not configured for session `{session_id}`")]
84 ProviderUnconfigured { session_id: String },
85 #[error("provider `{provider_id}` is not registered for session `{session_id}`")]
86 ProviderUnavailable {
87 provider_id: String,
88 session_id: String,
89 },
90 #[error("protocol error: {0}")]
91 Protocol(String),
92}
93
94#[derive(Clone, Debug)]
95pub struct ExecRequest {
96 pub language: String,
97 pub code: String,
98 pub accept_finish: bool,
99}
100
101pub struct Session {
102 session_id: String,
103 services: RuntimeServices,
104 include_base_tools: bool,
105 context_overlay_revision: u64,
106 context_tools: Vec<Arc<dyn ToolProvider>>,
107 tool_registry: Arc<crate::ToolRegistry>,
108 context_prompt_contributions: Vec<PromptContribution>,
109 message_tx: Option<UnboundedSender<SandboxMessage>>,
110 tool_catalog_cache: Arc<std::sync::Mutex<Vec<(ToolCatalogCacheKey, ToolCatalogHandle)>>>,
111 prompt_cache: Arc<lash_sansio::PromptCache>,
116}
117
118impl Session {
119 pub async fn new(services: RuntimeServices, session_id: &str) -> Result<Self, SessionError> {
120 let tool_registry = services.plugins.tool_registry();
121 let mut session = Self {
122 session_id: session_id.to_string(),
123 services,
124 include_base_tools: true,
125 context_overlay_revision: 0,
126 context_tools: Vec::new(),
127 tool_registry,
128 context_prompt_contributions: Vec::new(),
129 message_tx: None,
130 tool_catalog_cache: Arc::new(std::sync::Mutex::new(Vec::new())),
131 prompt_cache: Arc::new(lash_sansio::PromptCache::new()),
132 };
133
134 let protocol_session = Arc::clone(session.plugins().protocol_session());
135 protocol_session
136 .initialize_session(crate::plugin::ProtocolSessionContext::new(
137 &mut session,
138 session_id,
139 ))
140 .await?;
141
142 Ok(session)
143 }
144
145 pub(crate) fn clone_for_effect(&self) -> Self {
146 Self {
147 session_id: self.session_id.clone(),
148 services: self.services.clone(),
149 include_base_tools: self.include_base_tools,
150 context_overlay_revision: self.context_overlay_revision,
151 context_tools: self.context_tools.clone(),
152 tool_registry: Arc::clone(&self.tool_registry),
153 context_prompt_contributions: self.context_prompt_contributions.clone(),
154 message_tx: self.message_tx.clone(),
155 tool_catalog_cache: Arc::clone(&self.tool_catalog_cache),
156 prompt_cache: Arc::clone(&self.prompt_cache),
157 }
158 }
159
160 pub fn session_id(&self) -> &str {
161 &self.session_id
162 }
163
164 pub(crate) fn protocol_extra_prompt_contributions(&self) -> Vec<PromptContribution> {
165 Vec::new()
169 }
170
171 pub fn tools(&self) -> Arc<dyn ToolProvider> {
172 Arc::clone(&self.tool_registry) as Arc<dyn ToolProvider>
173 }
174
175 pub fn plugins(&self) -> &Arc<crate::PluginSession> {
176 &self.services.plugins
177 }
178
179 pub fn set_context_overlay(
180 &mut self,
181 tool_providers: Vec<Arc<dyn ToolProvider>>,
182 prompt_contributions: Vec<PromptContribution>,
183 include_base_tools: bool,
184 ) -> Result<(), crate::PluginError> {
185 let tool_providers_unchanged = self.context_tools.len() == tool_providers.len()
186 && self
187 .context_tools
188 .iter()
189 .zip(&tool_providers)
190 .all(|(current, next)| Arc::ptr_eq(current, next));
191 let overlay_unchanged = self.include_base_tools == include_base_tools
192 && self.context_prompt_contributions == prompt_contributions
193 && tool_providers_unchanged;
194 let registry = self
195 .services
196 .plugins
197 .tool_registry()
198 .compose_session_catalog(include_base_tools, tool_providers.clone())
199 .map(Arc::new)
200 .map_err(|err| {
201 crate::PluginError::Session(format!("failed to build session tool registry: {err}"))
202 })?;
203 self.include_base_tools = include_base_tools;
204 if !overlay_unchanged {
205 self.context_overlay_revision = self.context_overlay_revision.wrapping_add(1);
206 }
207 self.context_tools = tool_providers;
208 self.tool_registry = registry;
209 self.context_prompt_contributions = prompt_contributions;
210 self.tool_catalog_cache
211 .lock()
212 .expect("tool catalog cache lock")
213 .clear();
214 Ok(())
215 }
216
217 pub fn prompt_cache(&self) -> Arc<lash_sansio::PromptCache> {
218 Arc::clone(&self.prompt_cache)
219 }
220
221 pub fn context_prompt_contributions(&self) -> &[PromptContribution] {
222 &self.context_prompt_contributions
223 }
224
225 pub fn history_store(&self) -> Option<Arc<dyn crate::store::RuntimePersistence>> {
226 self.services.store.clone()
227 }
228
229 fn tool_catalog_cache_key(&self) -> ToolCatalogCacheKey {
230 ToolCatalogCacheKey {
231 include_base_tools: self.include_base_tools,
232 context_overlay_revision: self.context_overlay_revision,
233 tool_generation: self.tool_registry.generation(),
234 plugin_revision: self.plugins().snapshot_revision_fingerprint(),
235 }
236 }
237
238 fn build_tool_catalog_entry(
239 &self,
240 session_id: &str,
241 ) -> Result<ToolCatalogHandle, crate::PluginError> {
242 let provider = self.tools();
243 let tools = provider.tool_manifests();
244 let contract_provider = Arc::clone(&provider);
245 let resolve_contract: lash_sansio::ToolContractResolver =
246 Arc::new(move |name: &str| contract_provider.resolve_contract(name));
247 let tool_catalog = Arc::new(self.plugins().resolve_tool_catalog(
248 crate::plugin::ToolCatalogContext {
249 session_id: session_id.to_string(),
250 tools,
251 resolve_contract: Some(Arc::clone(&resolve_contract)),
252 tool_access: self.plugins().tool_access().clone(),
253 subagent: self.plugins().subagent_context().cloned(),
254 extensions: self.plugins().extensions().clone(),
255 },
256 )?);
257 let input = crate::ProtocolBuildInput {
258 tool_catalog: Arc::clone(&tool_catalog),
259 plugin_extensions: self.plugins().extensions().clone(),
260 trigger_events: self.plugins().triggers().clone(),
261 extra_prompt_contributions: self.protocol_extra_prompt_contributions(),
262 };
263 let driver = self.plugins().protocol_driver();
264 let preamble = driver.build_preamble(input);
265 Ok(ToolCatalogHandle(Arc::new(ToolCatalogArtifact {
266 tool_catalog,
267 preamble: Arc::new(preamble),
268 derived: ToolCatalogDerived::default(),
269 })))
270 }
271
272 fn tool_catalog_cache_entry(
273 &self,
274 session_id: &str,
275 ) -> Result<ToolCatalogHandle, crate::PluginError> {
276 let key = self.tool_catalog_cache_key();
277 let mut cache = self
278 .tool_catalog_cache
279 .lock()
280 .expect("tool catalog cache lock");
281 if let Some((_, entry)) = cache.iter().find(|(entry_key, _)| *entry_key == key) {
282 return Ok(entry.clone());
283 }
284 let entry = self.build_tool_catalog_entry(session_id)?;
285 cache.push((key, entry.clone()));
286 Ok(entry)
287 }
288
289 pub fn resolved_tool_catalog(
290 &self,
291 session_id: &str,
292 ) -> Result<Arc<crate::ToolCatalog>, crate::PluginError> {
293 Ok(self.tool_catalog_cache_entry(session_id)?.tool_catalog())
294 }
295
296 pub(crate) fn turn_driver_preamble(
297 &self,
298 session_id: &str,
299 ) -> Result<Arc<crate::TurnDriverPreamble>, crate::PluginError> {
300 Ok(self.tool_catalog_cache_entry(session_id)?.preamble())
301 }
302
303 pub(crate) fn shared_tool_catalog(
304 &self,
305 session_id: &str,
306 ) -> Result<Arc<Vec<serde_json::Value>>, crate::PluginError> {
307 Ok(self.tool_catalog_cache_entry(session_id)?.catalog())
308 }
309
310 pub fn tool_catalog(
311 &self,
312 session_id: &str,
313 ) -> Result<Vec<serde_json::Value>, crate::PluginError> {
314 Ok(self.shared_tool_catalog(session_id)?.as_ref().clone())
315 }
316
317 #[allow(
318 clippy::too_many_arguments,
319 reason = "code execution bridge carries explicit per-turn runtime dependencies"
320 )]
321 pub(crate) fn code_execution_context<'run>(
322 &self,
323 session_id: &str,
324 agent_frame_id: &str,
325 sessions: Arc<dyn crate::plugin::SessionStateService>,
326 session_lifecycle: Arc<dyn crate::plugin::SessionLifecycleService>,
327 session_graph: Arc<dyn crate::plugin::SessionGraphService>,
328 processes: Arc<dyn crate::ProcessService>,
329 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
330 effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
331 direct_completions: crate::DirectCompletionClient<'run>,
332 trigger_router: Option<crate::TriggerRouter>,
333 event_tx: tokio::sync::mpsc::Sender<SessionStreamEvent>,
334 chronological_projection: Arc<crate::ChronologicalProjection>,
335 protocol_extension: Option<crate::ProtocolTurnExtensionHandle>,
336 turn_context: crate::TurnContext,
337 execution_env_spec: crate::ProcessExecutionEnvSpec,
338 checkpoint_messages: crate::tool_dispatch::CheckpointMessageBuffer,
339 attachment_source_policy: Arc<dyn crate::AttachmentSourcePolicy>,
340 ) -> Result<RuntimeExecutionContext<'run>, crate::PluginError> {
341 let dispatch = Arc::new(ToolDispatchContext {
342 plugins: Arc::clone(self.plugins()),
343 tools: self.tools(),
344 tool_catalog: self.resolved_tool_catalog(session_id)?,
345 sessions,
346 session_lifecycle,
347 session_graph,
348 processes,
349 process_cancel_ability,
350 trigger_router,
351 effect_controller,
352 direct_completions: direct_completions.clone(),
353 parent_invocation: None,
354 execution_env_spec: execution_env_spec.clone(),
355 session_id: session_id.to_string(),
356 agent_frame_id: agent_frame_id.to_string(),
357 event_tx,
358 checkpoint_messages,
359 trigger_outcomes: crate::tool_dispatch::ToolTriggerOutcomeBuffer::default(),
360 attachment_store: Arc::clone(&self.services.attachment_store),
361 attachment_source_policy,
362 turn_context: turn_context.clone(),
363 clock: Arc::clone(&self.services.clock),
364 });
365 Ok(RuntimeExecutionContext::new(
366 session_id.to_string(),
367 dispatch,
368 Arc::clone(&self.services.process_env_store),
369 Arc::clone(&self.services.attachment_store),
370 chronological_projection,
371 protocol_extension,
372 turn_context,
373 ))
374 .map(|context| context.with_execution_env_spec(execution_env_spec))
375 }
376
377 pub fn set_message_sender(&mut self, tx: UnboundedSender<SandboxMessage>) {
379 self.message_tx = Some(tx);
380 }
381
382 pub fn clear_message_sender(&mut self) {
384 self.message_tx = None;
385 }
386
387 pub fn invalidate_runtime_caches(&self) {
388 self.tool_catalog_cache
389 .lock()
390 .expect("tool catalog cache lock")
391 .clear();
392 self.prompt_cache.clear();
393 }
394
395 pub async fn refresh_tool_catalog(&mut self) -> Result<(), SessionError> {
396 self.tool_registry = self
397 .services
398 .plugins
399 .tool_registry()
400 .compose_session_catalog(self.include_base_tools, self.context_tools.clone())
401 .map(Arc::new)
402 .map_err(|err| SessionError::Protocol(format!("tool reconfigure failed: {err}")))?;
403 self.tool_catalog_cache
404 .lock()
405 .expect("tool catalog cache lock")
406 .clear();
407 Ok(())
408 }
409}