Skip to main content

lash_core/session/
execution_context.rs

1use std::sync::Arc;
2
3use tokio::sync::mpsc::Sender;
4use tokio_util::sync::CancellationToken;
5
6use crate::tool_dispatch::ToolDispatchContext;
7use crate::{TurnActivity, TurnActivityId, TurnEvent};
8
9pub(crate) fn lashlang_surface_from_tool_surface(
10    surface: &crate::ToolSurface,
11    abilities: lashlang::LashlangAbilities,
12    language_features: lashlang::LashlangLanguageFeatures,
13    host_resources: lashlang::ResourceCatalog,
14) -> lashlang::LashlangSurface {
15    let mut resources = lashlang_resources_from_tool_surface(surface);
16    resources.extend(host_resources);
17    lashlang::LashlangSurface::new(resources, abilities).with_language_features(language_features)
18}
19
20pub(crate) fn lashlang_resources_from_tool_surface(
21    surface: &crate::ToolSurface,
22) -> lashlang::ResourceCatalog {
23    let mut catalog = lashlang::ResourceCatalog::new();
24    for entry in surface.tools.iter() {
25        if entry.availability.is_callable() {
26            let agent_surface = entry
27                .manifest
28                .agent_surface
29                .executable_for(&entry.manifest.name);
30            catalog.add_module_operation(
31                agent_surface.module_path.iter().map(String::as_str),
32                agent_surface.authority_type.clone(),
33                agent_surface.operation.clone(),
34                entry.manifest.name.clone(),
35                lashlang::TypeExpr::Any,
36                lashlang::TypeExpr::Any,
37            );
38        }
39    }
40    catalog
41}
42
43#[derive(Clone)]
44pub struct RuntimeExecutionContext<'run> {
45    pub(super) session_id: String,
46    pub(super) dispatch: Arc<ToolDispatchContext<'run>>,
47    lashlang_abilities: lashlang::LashlangAbilities,
48    lashlang_language_features: lashlang::LashlangLanguageFeatures,
49    lashlang_surface: lashlang::LashlangSurface,
50    lashlang_artifact_store: Arc<dyn lashlang::LashlangArtifactStore>,
51    attachment_store: Arc<dyn crate::AttachmentStore>,
52    chronological_projection: Arc<crate::ChronologicalProjection>,
53    protocol_extension: Option<crate::ProtocolTurnExtensionHandle>,
54    turn_context: crate::TurnContext,
55    execution_env_spec: crate::ProcessExecutionEnvSpec,
56    process_originator: Option<crate::ProcessOriginator>,
57    process_env_ref: Option<crate::ProcessExecutionEnvRef>,
58    process_wake_target: Option<crate::SessionScope>,
59    pub(super) parent_invocation: Option<crate::RuntimeInvocation>,
60    lashlang_execution_sink: Option<Arc<dyn lash_trace::TraceSink>>,
61    lashlang_execution_context: lash_trace::TraceContext,
62    pub(super) turn_event_tx: Option<Sender<TurnActivity>>,
63    pub(super) cancellation_token: Option<CancellationToken>,
64}
65
66impl<'run> RuntimeExecutionContext<'run> {
67    pub(crate) fn drain_tool_host_event_outcomes(
68        &self,
69    ) -> Result<Vec<crate::tool_dispatch::ToolHostEventEffectOutcome>, crate::PluginError> {
70        self.dispatch
71            .host_event_outcomes
72            .drain()
73            .map_err(crate::PluginError::Session)
74    }
75
76    pub(super) fn process_scope(
77        &self,
78        parent_invocation: Option<crate::RuntimeInvocation>,
79    ) -> crate::ProcessOpScope<'_> {
80        crate::ProcessOpScope::new(self.dispatch.effect_controller.scoped())
81            .with_parent_invocation(parent_invocation)
82            .with_agent_frame_id(Some(self.dispatch.agent_frame_id.clone()))
83    }
84
85    #[allow(
86        clippy::too_many_arguments,
87        reason = "code execution bridge carries explicit per-turn runtime dependencies"
88    )]
89    pub(crate) fn new(
90        session_id: String,
91        dispatch: Arc<ToolDispatchContext<'run>>,
92        lashlang_abilities: lashlang::LashlangAbilities,
93        lashlang_language_features: lashlang::LashlangLanguageFeatures,
94        lashlang_artifact_store: Arc<dyn lashlang::LashlangArtifactStore>,
95        attachment_store: Arc<dyn crate::AttachmentStore>,
96        chronological_projection: Arc<crate::ChronologicalProjection>,
97        protocol_extension: Option<crate::ProtocolTurnExtensionHandle>,
98        turn_context: crate::TurnContext,
99    ) -> Self {
100        let lashlang_surface = lashlang_surface_from_tool_surface(
101            &dispatch.surface,
102            lashlang_abilities,
103            lashlang_language_features,
104            dispatch.plugins.lashlang_resources(),
105        );
106        Self {
107            session_id,
108            dispatch,
109            lashlang_abilities,
110            lashlang_language_features,
111            lashlang_surface,
112            lashlang_artifact_store,
113            attachment_store,
114            chronological_projection,
115            protocol_extension,
116            turn_context,
117            execution_env_spec: crate::ProcessExecutionEnvSpec::new(
118                crate::PluginOptions::default(),
119                crate::SessionPolicy::default(),
120            ),
121            process_originator: None,
122            process_env_ref: None,
123            process_wake_target: None,
124            parent_invocation: None,
125            lashlang_execution_sink: None,
126            lashlang_execution_context: lash_trace::TraceContext::default(),
127            turn_event_tx: None,
128            cancellation_token: None,
129        }
130    }
131
132    pub fn session_id(&self) -> &str {
133        &self.session_id
134    }
135
136    pub fn attachment_store(&self) -> Arc<dyn crate::AttachmentStore> {
137        Arc::clone(&self.attachment_store)
138    }
139
140    pub async fn put_lashlang_module_artifact(
141        &self,
142        artifact: &lashlang::ModuleArtifact,
143    ) -> Result<(), String> {
144        self.lashlang_artifact_store
145            .put_module_artifact(artifact)
146            .await
147            .map_err(|err| err.to_string())
148    }
149
150    pub fn chronological_projection(&self) -> Arc<crate::ChronologicalProjection> {
151        Arc::clone(&self.chronological_projection)
152    }
153
154    pub fn protocol_extension<T: 'static>(&self) -> Option<&T> {
155        self.protocol_extension
156            .as_ref()
157            .and_then(|extension| extension.as_any().downcast_ref::<T>())
158    }
159
160    pub fn turn_context(&self) -> &crate::TurnContext {
161        &self.turn_context
162    }
163
164    pub(crate) fn session_graph_service(&self) -> &dyn crate::plugin::SessionGraphService {
165        self.dispatch.session_graph.as_ref()
166    }
167
168    pub(super) async fn emit_turn_activity(
169        &self,
170        correlation_id: TurnActivityId,
171        event: TurnEvent,
172    ) {
173        if let Some(tx) = &self.turn_event_tx {
174            let _ = tx.send(TurnActivity::new(correlation_id, event)).await;
175        }
176    }
177
178    pub(crate) fn with_turn_event_sender(mut self, turn_event_tx: Sender<TurnActivity>) -> Self {
179        self.turn_event_tx = Some(turn_event_tx);
180        self
181    }
182
183    pub(crate) fn with_parent_invocation(mut self, metadata: crate::RuntimeInvocation) -> Self {
184        self.parent_invocation = Some(metadata);
185        self
186    }
187
188    pub(crate) fn with_execution_env_spec(
189        mut self,
190        execution_env_spec: crate::ProcessExecutionEnvSpec,
191    ) -> Self {
192        self.execution_env_spec = execution_env_spec;
193        self
194    }
195
196    pub(crate) fn with_process_registration_context(
197        mut self,
198        registration: &crate::ProcessRegistration,
199    ) -> Self {
200        self.process_originator = Some(registration.provenance.originator.clone());
201        self.process_env_ref = registration.env_ref.clone();
202        self.process_wake_target = registration.wake_target.clone();
203        self
204    }
205
206    pub(super) async fn attach_captured_process_execution_env(
207        &self,
208        registration: crate::ProcessRegistration,
209    ) -> Result<crate::ProcessRegistration, crate::PluginError> {
210        if registration.env_ref.is_some() {
211            return Ok(registration);
212        }
213        match registration.input.as_ref() {
214            crate::ProcessInput::ToolCall { .. } | crate::ProcessInput::LashlangProcess { .. } => {
215                let env_ref = self.captured_process_execution_env_ref().await?;
216                Ok(registration.with_execution_env_ref(Some(env_ref)))
217            }
218            crate::ProcessInput::External { .. } | crate::ProcessInput::SessionTurn { .. } => {
219                Ok(registration)
220            }
221        }
222    }
223
224    async fn captured_process_execution_env_ref(
225        &self,
226    ) -> Result<crate::ProcessExecutionEnvRef, crate::PluginError> {
227        if let Some(env_ref) = self.process_env_ref.clone() {
228            return Ok(env_ref);
229        }
230        crate::persist_process_execution_env(
231            self.lashlang_artifact_store.as_ref(),
232            &self.execution_env_spec,
233        )
234        .await
235    }
236
237    pub(crate) fn with_lashlang_execution_trace(
238        mut self,
239        sink: Option<Arc<dyn lash_trace::TraceSink>>,
240        context: lash_trace::TraceContext,
241    ) -> Self {
242        self.lashlang_execution_sink = sink;
243        self.lashlang_execution_context = context;
244        self
245    }
246
247    pub fn parent_invocation(&self) -> Option<&crate::RuntimeInvocation> {
248        self.parent_invocation.as_ref()
249    }
250
251    pub fn lashlang_execution_sink(&self) -> Option<Arc<dyn lash_trace::TraceSink>> {
252        self.lashlang_execution_sink.clone()
253    }
254
255    pub fn lashlang_execution_context(&self) -> &lash_trace::TraceContext {
256        &self.lashlang_execution_context
257    }
258
259    pub(crate) fn with_cancellation_token(mut self, cancellation_token: CancellationToken) -> Self {
260        self.cancellation_token = Some(cancellation_token);
261        self
262    }
263
264    pub(crate) fn tool_scheduling(&self, name: &str) -> crate::ToolScheduling {
265        crate::tool_dispatch::resolve_tool_scheduling(&self.dispatch, name)
266    }
267
268    pub fn callable_tool_manifest(&self, name: &str) -> Option<crate::ToolManifest> {
269        crate::tool_dispatch::resolve_callable_manifest(&self.dispatch, name)
270    }
271
272    pub fn callable_tool_manifest_by_id(&self, id: &crate::ToolId) -> Option<crate::ToolManifest> {
273        crate::tool_dispatch::resolve_callable_manifest_by_id(&self.dispatch, id)
274    }
275
276    pub fn resolve_lashlang_host_operation(
277        &self,
278        receiver: &lashlang::ResourceHandle,
279        operation: &str,
280    ) -> Result<String, String> {
281        self.lashlang_surface
282            .resources
283            .resolve_module_operation(&receiver.resource_type, &receiver.alias, operation)
284            .map(|binding| binding.host_operation.clone())
285            .ok_or_else(|| {
286                format!(
287                    "module `{}` of type `{}` does not expose operation `{operation}`",
288                    receiver.alias, receiver.resource_type
289                )
290            })
291    }
292
293    pub async fn prepare_lashlang_process_start(
294        &self,
295        start: lashlang::ProcessStart,
296    ) -> Result<(crate::ProcessRegistration, Option<String>), String> {
297        let display_name = Some(start.process_name.clone());
298        let artifact = self
299            .lashlang_artifact_store
300            .get_module_artifact(&start.module_ref)
301            .await
302            .map_err(|err| format!("failed to load lashlang module artifact: {err}"))?
303            .ok_or_else(|| {
304                format!(
305                    "missing lashlang module artifact `{}` for process `{}`",
306                    start.module_ref, start.process_name
307                )
308            })?;
309        if artifact.required_surface_ref != start.required_surface_ref {
310            return Err(format!(
311                "lashlang module artifact `{}` required surface mismatch: process requested {}, artifact has {}",
312                start.module_ref, start.required_surface_ref, artifact.required_surface_ref
313            ));
314        }
315        if artifact.process_ref(&start.process_name) != Some(&start.process_ref) {
316            return Err(format!(
317                "lashlang module artifact `{}` does not export process `{}` as requested ref {:?}",
318                start.module_ref, start.process_name, start.process_ref
319            ));
320        }
321        let args = match serde_json::to_value(lashlang::Value::Record(Arc::new(start.args)))
322            .map_err(|err| format!("failed to serialize process args: {err}"))?
323        {
324            serde_json::Value::Object(map) => map,
325            _ => return Err("process args must serialize as a record".to_string()),
326        };
327        let signal_event_types = artifact
328            .canonical_ir
329            .process(&start.process_name)
330            .map(crate::lashlang_process_signal_event_types)
331            .unwrap_or_default();
332        let process_id = format!("process:{}", uuid::Uuid::new_v4());
333        let registration = crate::ProcessRegistration::session_start_draft(
334            process_id,
335            crate::ProcessInput::LashlangProcess {
336                module_ref: start.module_ref,
337                process_ref: start.process_ref,
338                required_surface_ref: start.required_surface_ref,
339                process_name: start.process_name,
340                args,
341            },
342        )
343        .with_extra_event_types(
344            crate::lashlang_process_event_types()
345                .into_iter()
346                .chain(signal_event_types),
347        );
348        Ok((registration, display_name))
349    }
350
351    pub fn lashlang_surface(&self) -> &lashlang::LashlangSurface {
352        &self.lashlang_surface
353    }
354
355    pub fn lashlang_abilities(&self) -> lashlang::LashlangAbilities {
356        self.lashlang_abilities
357    }
358
359    pub fn lashlang_language_features(&self) -> lashlang::LashlangLanguageFeatures {
360        self.lashlang_language_features
361    }
362
363    pub fn link_lashlang_module(
364        &self,
365        program: lashlang::Program,
366    ) -> Result<lashlang::LinkedModule, String> {
367        lashlang::LinkedModule::link(program, self.lashlang_surface())
368            .map_err(|err| err.to_string())
369    }
370
371    pub async fn perform_lashlang_trigger_operation(
372        &self,
373        operation: &str,
374        payload: serde_json::Value,
375    ) -> Result<serde_json::Value, String> {
376        match lashlang::TriggerHostOperation::from_host_operation(operation) {
377            Some(lashlang::TriggerHostOperation::Register) => self
378                .register_host_event_subscription(payload)
379                .await
380                .map_err(|err| err.to_string()),
381            Some(lashlang::TriggerHostOperation::List) => self
382                .list_host_event_subscriptions(payload)
383                .await
384                .map_err(|err| err.to_string()),
385            Some(lashlang::TriggerHostOperation::Cancel) => self
386                .cancel_host_event_subscription(payload)
387                .await
388                .map_err(|err| err.to_string()),
389            None => Err(format!("unknown trigger operation `{operation}`")),
390        }
391    }
392
393    async fn register_host_event_subscription(
394        &self,
395        payload: serde_json::Value,
396    ) -> Result<serde_json::Value, crate::PluginError> {
397        let router = self.dispatch.host_event_router.as_ref().ok_or_else(|| {
398            crate::PluginError::Session(
399                "host event store is unavailable in this runtime".to_string(),
400            )
401        })?;
402        let request = lashlang::TriggerRegistrationRequest::decode(&payload)
403            .map_err(|err| crate::PluginError::Session(err.to_string()))?;
404        let source_type = request.source.source_type.clone();
405        let source_value = request.source.value.clone();
406        let source = request.source.to_json();
407        let event_type = lashlang::event_type_for_source(
408            &self.dispatch.plugins.lashlang_resources(),
409            &source_type,
410        )
411        .map_err(|err| crate::PluginError::Session(err.to_string()))?;
412        let validation = crate::plugin::validate_target_process(
413            &request.target,
414            &event_type,
415            &request.inputs,
416            self.lashlang_artifact_store.as_ref(),
417        )
418        .await?;
419        let store = router.store();
420        let source_key = store
421            .source_key_for_subscription(&source_type, &source_value)
422            .await?;
423        let env_ref = match self.process_env_ref.clone() {
424            Some(env_ref) => env_ref,
425            None => {
426                crate::persist_process_execution_env(
427                    self.lashlang_artifact_store.as_ref(),
428                    &self.execution_env_spec,
429                )
430                .await?
431            }
432        };
433        let registrant = self.process_originator.clone().unwrap_or_else(|| {
434            crate::ProcessOriginator::session(crate::SessionScope::new(self.session_id.clone()))
435        });
436        let wake_target = self
437            .process_wake_target
438            .clone()
439            .or_else(|| match &registrant {
440                crate::ProcessOriginator::Session { scope } => Some(scope.clone()),
441                crate::ProcessOriginator::Host => None,
442            });
443        let record = store
444            .register_subscription(crate::TriggerSubscriptionDraft {
445                registrant,
446                env_ref,
447                wake_target,
448                name: request.name,
449                source_type,
450                source_key,
451                source,
452                event_ty: validation.event_ty,
453                module_ref: request.target.module_ref,
454                required_surface_ref: request.target.required_surface_ref,
455                process_ref: request.target.process_ref,
456                process_name: request.target.process_name,
457                input_template: validation.inputs,
458            })
459            .await?;
460        Ok(crate::plugin::trigger_handle_json(&record.handle))
461    }
462
463    async fn list_host_event_subscriptions(
464        &self,
465        payload: serde_json::Value,
466    ) -> Result<serde_json::Value, crate::PluginError> {
467        let router = self.dispatch.host_event_router.as_ref().ok_or_else(|| {
468            crate::PluginError::Session(
469                "host event store is unavailable in this runtime".to_string(),
470            )
471        })?;
472        let request = lashlang::TriggerListRequest::decode(&payload)
473            .map_err(|err| crate::PluginError::Session(err.to_string()))?;
474        let mut filter = crate::TriggerSubscriptionFilter::for_session(&self.session_id);
475        filter.target = request.target;
476        filter.name = request.name;
477        filter.source_type = request.source_type;
478        filter.enabled = request.enabled;
479        let registrations = router
480            .store()
481            .list_subscriptions(filter)
482            .await?
483            .iter()
484            .map(crate::TriggerRegistration::from)
485            .collect::<Vec<_>>();
486        serde_json::to_value(registrations).map_err(|err| {
487            crate::PluginError::Session(format!("failed to encode trigger registrations: {err}"))
488        })
489    }
490
491    async fn cancel_host_event_subscription(
492        &self,
493        payload: serde_json::Value,
494    ) -> Result<serde_json::Value, crate::PluginError> {
495        let router = self.dispatch.host_event_router.as_ref().ok_or_else(|| {
496            crate::PluginError::Session(
497                "host event store is unavailable in this runtime".to_string(),
498            )
499        })?;
500        let request = lashlang::TriggerCancelRequest::decode(&payload)
501            .map_err(|err| crate::PluginError::Session(err.to_string()))?;
502        let changed = router
503            .store()
504            .cancel_subscription(&self.session_id, &request.handle)
505            .await?;
506        Ok(serde_json::json!(changed))
507    }
508
509    pub fn tool_argument_projection_policy(
510        &self,
511        name: &str,
512    ) -> crate::ToolArgumentProjectionPolicy {
513        crate::tool_dispatch::resolve_tool_argument_projection_policy(&self.dispatch, name)
514    }
515
516    pub async fn start_lashlang_process(
517        &self,
518        registration: crate::ProcessRegistration,
519        label: Option<String>,
520    ) -> crate::ToolInvocationReply {
521        let registration = match self
522            .attach_captured_process_execution_env(registration)
523            .await
524        {
525            Ok(registration) => registration,
526            Err(err) => {
527                return crate::ToolInvocationReply::error(serde_json::json!(err.to_string()));
528            }
529        };
530        let process_id = registration.id.clone();
531        match self
532            .dispatch
533            .processes
534            .start(
535                &self.session_id,
536                registration,
537                crate::ProcessStartOptions::new()
538                    .with_descriptor(crate::ProcessHandleDescriptor::new(Some("lashlang"), label)),
539                self.process_scope(self.parent_invocation.clone()),
540            )
541            .await
542        {
543            Ok(_) => crate::ToolInvocationReply::success(
544                crate::lashlang_bridge::process_handle_json(&process_id),
545            ),
546            Err(err) => crate::ToolInvocationReply::error(serde_json::json!(err.to_string())),
547        }
548    }
549
550    pub async fn sleep_lashlang(
551        &self,
552        scope: &str,
553        sequence: u64,
554        duration_ms: u64,
555    ) -> Result<(), crate::RuntimeEffectControllerError> {
556        let cancellation = self.cancellation_token.clone().unwrap_or_default();
557        let invocation = crate::runtime::causal::lashlang_sleep_invocation(
558            &self.session_id,
559            self.parent_invocation.as_ref(),
560            scope,
561            sequence,
562        );
563        let outcome = self
564            .dispatch
565            .effect_controller
566            .controller()
567            .execute_effect(
568                crate::RuntimeEffectEnvelope::new(
569                    invocation,
570                    crate::RuntimeEffectCommand::Sleep { duration_ms },
571                ),
572                crate::RuntimeEffectLocalExecutor::sleep(cancellation),
573            )
574            .await?;
575        match outcome {
576            crate::RuntimeEffectOutcome::Sleep => Ok(()),
577            other => Err(crate::RuntimeEffectControllerError::new(
578                "runtime_effect_wrong_outcome",
579                format!("expected sleep outcome, got {}", other.kind().as_str()),
580            )),
581        }
582    }
583
584    pub async fn await_process_event_lashlang(
585        &self,
586        registry: Arc<dyn crate::ProcessRegistry>,
587        process_id: &str,
588        signal_name: &str,
589        event_type: &str,
590        event_ordinal: u64,
591    ) -> Result<serde_json::Value, crate::RuntimeEffectControllerError> {
592        let cancellation = self.cancellation_token.clone().unwrap_or_default();
593        let key = crate::process_signal_wait_key(process_id, signal_name, event_ordinal);
594        let invocation = crate::runtime::causal::lashlang_await_event_invocation(
595            &self.session_id,
596            self.parent_invocation.as_ref(),
597            process_id,
598            signal_name,
599            event_ordinal,
600        );
601        let outcome = self
602            .dispatch
603            .effect_controller
604            .controller()
605            .execute_effect(
606                crate::RuntimeEffectEnvelope::new(
607                    invocation,
608                    crate::RuntimeEffectCommand::AwaitEvent { key: key.clone() },
609                ),
610                crate::RuntimeEffectLocalExecutor::await_process_event(
611                    key,
612                    registry,
613                    process_id.to_string(),
614                    event_type.to_string(),
615                    event_ordinal,
616                    cancellation,
617                ),
618            )
619            .await?;
620        outcome.into_await_event()
621    }
622
623    pub async fn signal_lashlang_process(
624        &self,
625        registry: Arc<dyn crate::ProcessRegistry>,
626        process_id: &str,
627        signal_name: &str,
628        signal_id: String,
629        payload: serde_json::Value,
630    ) -> Result<crate::ProcessEvent, crate::RuntimeEffectControllerError> {
631        let event_type = crate::process_signal_event_type(signal_name)?;
632        let replay_key = format!("process:{process_id}:signal.{signal_name}:{signal_id}");
633        let command = crate::ProcessCommand::Signal {
634            process_id: process_id.to_string(),
635            signal_name: signal_name.to_string(),
636            signal_id,
637            request: crate::ProcessEventAppendRequest::new(event_type, payload)
638                .with_replay_key(replay_key),
639        };
640        let effect_id = command.effect_id();
641        let invocation = crate::runtime::causal::process_effect_invocation(
642            &self.session_id,
643            self.parent_invocation.clone(),
644            &effect_id,
645        );
646        let outcome = self
647            .dispatch
648            .effect_controller
649            .controller()
650            .execute_effect(
651                crate::RuntimeEffectEnvelope::new(
652                    invocation,
653                    crate::RuntimeEffectCommand::process(command),
654                ),
655                crate::RuntimeEffectLocalExecutor::process_control(registry),
656            )
657            .await?;
658        match outcome.into_process()? {
659            crate::ProcessEffectOutcome::Signal { event } => Ok(event),
660            other => Err(crate::RuntimeEffectControllerError::new(
661                "runtime_effect_wrong_outcome",
662                format!("expected signal outcome, got {other:?}"),
663            )),
664        }
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use crate::tool_dispatch::ToolDispatchContext;
672    use crate::{ToolCall, ToolProvider, ToolResult};
673
674    struct NoopTools;
675
676    #[async_trait::async_trait]
677    impl ToolProvider for NoopTools {
678        fn tool_manifests(&self) -> Vec<crate::ToolManifest> {
679            Vec::new()
680        }
681
682        fn resolve_contract(&self, _name: &str) -> Option<Arc<crate::ToolContract>> {
683            None
684        }
685
686        async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
687            ToolResult::err_fmt("not used")
688        }
689    }
690
691    #[test]
692    fn tool_argument_projection_policy_resolves_from_active_surface_and_defaults_unknown() {
693        let tool = crate::ToolDefinition::raw(
694            "tool:seedy",
695            "seedy",
696            "Seed-aware",
697            crate::ToolDefinition::default_input_schema(),
698            serde_json::json!({ "type": "string" }),
699        )
700        .with_argument_projection(
701            crate::ToolArgumentProjectionPolicy::preserve_projected_refs_in_field("seed"),
702        );
703        let plugins = crate::plugin::PluginHost::empty()
704            .build_session("session", None)
705            .expect("plugin session");
706        let (event_tx, _event_rx) = tokio::sync::mpsc::channel(1);
707        let dispatch = Arc::new(ToolDispatchContext {
708            plugins,
709            tools: Arc::new(NoopTools),
710            surface: Arc::new(crate::ToolSurface::from_tools(
711                vec![tool.manifest()],
712                std::collections::BTreeMap::new(),
713            )),
714            sessions: Arc::new(crate::testing::MockSessionManager::default()),
715            session_lifecycle: Arc::new(crate::testing::MockSessionManager::default()),
716            session_graph: Arc::new(crate::testing::MockSessionManager::default()),
717            processes: Arc::new(crate::UnavailableProcessService),
718            process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
719            host_event_router: None,
720            effect_controller: crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
721                crate::InlineRuntimeEffectController,
722            )),
723            direct_completions: crate::DirectCompletionClient::unavailable(
724                "direct completions are unavailable in this test context",
725            ),
726            parent_invocation: None,
727            execution_env_spec: crate::ProcessExecutionEnvSpec::new(
728                crate::PluginOptions::default(),
729                crate::SessionPolicy::default(),
730            ),
731            session_id: "session".to_string(),
732            agent_frame_id: String::new(),
733            event_tx,
734            checkpoint_messages: crate::tool_dispatch::CheckpointMessageBuffer::default(),
735            host_event_outcomes: crate::tool_dispatch::ToolHostEventOutcomeBuffer::default(),
736            attachment_store: Arc::new(crate::InMemoryAttachmentStore::new()),
737            turn_context: crate::TurnContext::default(),
738        });
739        let ctx = RuntimeExecutionContext::new(
740            "session".to_string(),
741            dispatch,
742            Default::default(),
743            Default::default(),
744            Arc::new(lashlang::InMemoryLashlangArtifactStore::new()),
745            Arc::new(crate::InMemoryAttachmentStore::new()),
746            Arc::new(crate::ChronologicalProjection::default()),
747            None,
748            crate::TurnContext::default(),
749        );
750
751        assert_eq!(
752            ctx.tool_argument_projection_policy("seedy"),
753            crate::ToolArgumentProjectionPolicy::preserve_projected_refs_in_field("seed")
754        );
755        assert_eq!(
756            ctx.tool_argument_projection_policy("missing"),
757            crate::ToolArgumentProjectionPolicy::MaterializeProjectedValues
758        );
759    }
760
761    #[tokio::test]
762    async fn prepare_lashlang_process_start_captures_tool_ids_and_explicit_input() {
763        let tool = crate::ToolDefinition::raw(
764            "tool:alpha",
765            "alpha",
766            "Alpha tool.",
767            crate::ToolDefinition::default_input_schema(),
768            serde_json::json!({ "type": "object", "additionalProperties": true }),
769        );
770        let plugins = crate::plugin::PluginHost::empty()
771            .build_session("session", None)
772            .expect("plugin session");
773        let (event_tx, _event_rx) = tokio::sync::mpsc::channel(1);
774        let dispatch = Arc::new(ToolDispatchContext {
775            plugins,
776            tools: Arc::new(NoopTools),
777            surface: Arc::new(crate::ToolSurface::from_tools(
778                vec![tool.manifest()],
779                std::collections::BTreeMap::new(),
780            )),
781            sessions: Arc::new(crate::testing::MockSessionManager::default()),
782            session_lifecycle: Arc::new(crate::testing::MockSessionManager::default()),
783            session_graph: Arc::new(crate::testing::MockSessionManager::default()),
784            processes: Arc::new(crate::UnavailableProcessService),
785            process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
786            host_event_router: None,
787            effect_controller: crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
788                crate::InlineRuntimeEffectController,
789            )),
790            direct_completions: crate::DirectCompletionClient::unavailable(
791                "direct completions are unavailable in this test context",
792            ),
793            parent_invocation: None,
794            execution_env_spec: crate::ProcessExecutionEnvSpec::new(
795                crate::PluginOptions::default(),
796                crate::SessionPolicy::default(),
797            ),
798            session_id: "session".to_string(),
799            agent_frame_id: String::new(),
800            event_tx,
801            checkpoint_messages: crate::tool_dispatch::CheckpointMessageBuffer::default(),
802            host_event_outcomes: crate::tool_dispatch::ToolHostEventOutcomeBuffer::default(),
803            attachment_store: Arc::new(crate::InMemoryAttachmentStore::new()),
804            turn_context: crate::TurnContext::default(),
805        });
806        let ctx = RuntimeExecutionContext::new(
807            "session".to_string(),
808            dispatch,
809            lashlang::LashlangAbilities::default().with_processes(),
810            Default::default(),
811            Arc::new(lashlang::InMemoryLashlangArtifactStore::new()),
812            Arc::new(crate::InMemoryAttachmentStore::new()),
813            Arc::new(crate::ChronologicalProjection::default()),
814            None,
815            crate::TurnContext::default(),
816        );
817        let mut input = lashlang::Record::new();
818        input.insert("root".to_string(), lashlang::Value::String(".".into()));
819        let linked = ctx
820            .link_lashlang_module(
821                lashlang::parse("process scan(root: str) { finish root }").expect("process module"),
822            )
823            .expect("link process module");
824        ctx.put_lashlang_module_artifact(&linked.artifact)
825            .await
826            .expect("store module artifact");
827        let process_ref = linked
828            .artifact
829            .process_ref("scan")
830            .expect("scan process ref")
831            .clone();
832        let (registration, label) = ctx
833            .prepare_lashlang_process_start(lashlang::ProcessStart {
834                module_ref: linked.module_ref.clone(),
835                process_ref,
836                required_surface_ref: linked.required_surface_ref.clone(),
837                process_name: "scan".to_string(),
838                args: input,
839            })
840            .await
841            .expect("process start should prepare");
842
843        assert_eq!(label.as_deref(), Some("scan"));
844        assert!(
845            registration
846                .event_types
847                .iter()
848                .any(|event_type| event_type.name == "process.wake")
849        );
850        let crate::ProcessInput::LashlangProcess {
851            args, process_name, ..
852        } = registration.input.as_ref()
853        else {
854            panic!("expected lashlang process input");
855        };
856        assert_eq!(process_name, "scan");
857        assert_eq!(args.get("root"), Some(&serde_json::json!(".")));
858    }
859
860    #[test]
861    fn lashlang_surface_reflects_host_abilities() {
862        let tool = crate::ToolDefinition::raw(
863            "tool:alpha",
864            "alpha",
865            "Alpha tool.",
866            crate::ToolDefinition::default_input_schema(),
867            serde_json::json!({ "type": "object", "additionalProperties": true }),
868        );
869        let plugins = crate::plugin::PluginHost::empty()
870            .build_session("session", None)
871            .expect("plugin session");
872        let (event_tx, _event_rx) = tokio::sync::mpsc::channel(1);
873        let dispatch = Arc::new(ToolDispatchContext {
874            plugins,
875            tools: Arc::new(NoopTools),
876            surface: Arc::new(crate::ToolSurface::from_tools(
877                vec![tool.manifest()],
878                std::collections::BTreeMap::new(),
879            )),
880            sessions: Arc::new(crate::testing::MockSessionManager::default()),
881            session_lifecycle: Arc::new(crate::testing::MockSessionManager::default()),
882            session_graph: Arc::new(crate::testing::MockSessionManager::default()),
883            processes: Arc::new(crate::UnavailableProcessService),
884            process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
885            host_event_router: None,
886            effect_controller: crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
887                crate::InlineRuntimeEffectController,
888            )),
889            direct_completions: crate::DirectCompletionClient::unavailable(
890                "direct completions are unavailable in this test context",
891            ),
892            parent_invocation: None,
893            execution_env_spec: crate::ProcessExecutionEnvSpec::new(
894                crate::PluginOptions::default(),
895                crate::SessionPolicy::default(),
896            ),
897            session_id: "session".to_string(),
898            agent_frame_id: String::new(),
899            event_tx,
900            checkpoint_messages: crate::tool_dispatch::CheckpointMessageBuffer::default(),
901            host_event_outcomes: crate::tool_dispatch::ToolHostEventOutcomeBuffer::default(),
902            attachment_store: Arc::new(crate::InMemoryAttachmentStore::new()),
903            turn_context: crate::TurnContext::default(),
904        });
905        let ctx = RuntimeExecutionContext::new(
906            "session".to_string(),
907            dispatch,
908            lashlang::LashlangAbilities::default()
909                .with_sleep()
910                .with_processes()
911                .with_process_signals(),
912            Default::default(),
913            Arc::new(lashlang::InMemoryLashlangArtifactStore::new()),
914            Arc::new(crate::InMemoryAttachmentStore::new()),
915            Arc::new(crate::ChronologicalProjection::default()),
916            None,
917            crate::TurnContext::default(),
918        );
919
920        let surface = ctx.lashlang_surface();
921
922        assert!(std::ptr::eq(surface, ctx.lashlang_surface()));
923        assert!(surface.abilities.processes);
924        assert!(surface.abilities.sleep);
925        assert!(surface.abilities.process_signals);
926        assert!(!surface.abilities.triggers);
927        assert!(
928            surface
929                .resources
930                .resolve_operation("Tools", "alpha")
931                .is_some()
932        );
933    }
934
935    #[test]
936    fn lashlang_surface_reflects_host_resource_contributions() {
937        let mut resources = lashlang::ResourceCatalog::new();
938        resources
939            .add_trigger_source_constructor(
940                ["clock", "Alarm"],
941                lashlang::TypeExpr::Object(vec![lashlang::TypeField {
942                    name: "at".into(),
943                    ty: lashlang::TypeExpr::Str,
944                    optional: false,
945                }]),
946                lashlang::NamedDataType::object(
947                    "clock.Tick",
948                    vec![lashlang::TypeField {
949                        name: "fired_at".into(),
950                        ty: lashlang::TypeExpr::Str,
951                        optional: false,
952                    }],
953                )
954                .expect("valid clock tick type"),
955            )
956            .expect("valid clock trigger source");
957        let plugin_host = crate::plugin::PluginHost::empty();
958        let mut merged_resources = plugin_host.lashlang_resources();
959        merged_resources.extend(resources);
960        let plugins = plugin_host
961            .with_lashlang_resources(merged_resources)
962            .build_session("session", None)
963            .expect("plugin session");
964        let (event_tx, _event_rx) = tokio::sync::mpsc::channel(1);
965        let dispatch = Arc::new(ToolDispatchContext {
966            plugins,
967            tools: Arc::new(NoopTools),
968            surface: Arc::new(crate::ToolSurface::from_tools(
969                Vec::new(),
970                std::collections::BTreeMap::new(),
971            )),
972            sessions: Arc::new(crate::testing::MockSessionManager::default()),
973            session_lifecycle: Arc::new(crate::testing::MockSessionManager::default()),
974            session_graph: Arc::new(crate::testing::MockSessionManager::default()),
975            processes: Arc::new(crate::UnavailableProcessService),
976            process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
977            host_event_router: None,
978            effect_controller: crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
979                crate::InlineRuntimeEffectController,
980            )),
981            direct_completions: crate::DirectCompletionClient::unavailable(
982                "direct completions are unavailable in this test context",
983            ),
984            parent_invocation: None,
985            execution_env_spec: crate::ProcessExecutionEnvSpec::new(
986                crate::PluginOptions::default(),
987                crate::SessionPolicy::default(),
988            ),
989            session_id: "session".to_string(),
990            agent_frame_id: String::new(),
991            event_tx,
992            checkpoint_messages: crate::tool_dispatch::CheckpointMessageBuffer::default(),
993            host_event_outcomes: crate::tool_dispatch::ToolHostEventOutcomeBuffer::default(),
994            attachment_store: Arc::new(crate::InMemoryAttachmentStore::new()),
995            turn_context: crate::TurnContext::default(),
996        });
997        let ctx = RuntimeExecutionContext::new(
998            "session".to_string(),
999            dispatch,
1000            lashlang::LashlangAbilities::default()
1001                .with_processes()
1002                .with_triggers(),
1003            Default::default(),
1004            Arc::new(lashlang::InMemoryLashlangArtifactStore::new()),
1005            Arc::new(crate::InMemoryAttachmentStore::new()),
1006            Arc::new(crate::ChronologicalProjection::default()),
1007            None,
1008            crate::TurnContext::default(),
1009        );
1010
1011        let surface = ctx.lashlang_surface();
1012
1013        assert!(
1014            surface
1015                .resources
1016                .resolve_value_constructor(&["clock", "Alarm"])
1017                .is_some()
1018        );
1019        assert!(
1020            surface
1021                .resources
1022                .resolve_trigger_source("clock.Alarm")
1023                .is_some()
1024        );
1025        lashlang::LinkedModule::link(
1026            lashlang::parse(
1027                r#"
1028                process remember(tick: clock.Tick) {
1029                  finish true
1030                }
1031
1032                source = clock.Alarm({ at: "08:00" })
1033                await triggers.register({
1034                  source: source,
1035                  target: remember,
1036                  inputs: { tick: trigger.event }
1037                })?
1038                "#,
1039            )
1040            .expect("parse trigger registry module"),
1041            surface,
1042        )
1043        .expect("host resource contribution should be linkable");
1044    }
1045}