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 ®istrant {
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}