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
9#[derive(Clone)]
10pub struct RuntimeExecutionContext<'run> {
11 pub(super) session_id: String,
12 pub(super) dispatch: Arc<ToolDispatchContext<'run>>,
13 process_env_store: Arc<dyn crate::ProcessExecutionEnvStore>,
14 attachment_store: Arc<crate::SessionAttachmentStore>,
15 chronological_projection: Arc<crate::ChronologicalProjection>,
16 protocol_extension: Option<crate::ProtocolTurnExtensionHandle>,
17 turn_context: crate::TurnContext,
18 execution_env_spec: crate::ProcessExecutionEnvSpec,
19 process_originator: Option<crate::ProcessOriginator>,
20 pub(super) runtime_process_id: Option<String>,
21 pub(super) process_event_context: Option<RuntimeExecutionProcessEventContext>,
22 process_env_ref: Option<crate::ProcessExecutionEnvRef>,
23 process_wake_target: Option<crate::SessionScope>,
24 pub(super) parent_invocation: Option<crate::RuntimeInvocation>,
25 turn_phase_probe: Option<Arc<dyn crate::runtime::RuntimeTurnPhaseProbe>>,
26 pub(super) turn_event_tx: Option<Sender<TurnActivity>>,
27 pub(super) cancellation_token: Option<CancellationToken>,
28 observe_turn_cancel: bool,
29 tracing: Option<RuntimeExecutionTracing>,
32 code_block_graph_key: Option<String>,
37 batch_parent_call_id: Option<String>,
41 process_work_driver: Option<crate::ProcessWorkDriver>,
47 started_process_ids: Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
52}
53
54#[derive(Clone)]
55pub(super) struct RuntimeExecutionProcessEventContext {
56 pub process_id: String,
57 pub registry: Arc<dyn crate::ProcessRegistry>,
58 pub awaiter: crate::ProcessAwaiter,
59 pub store: Option<Arc<dyn crate::RuntimePersistence>>,
60 pub session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
61 pub queued_work_driver: Option<crate::QueuedWorkDriver>,
62}
63
64#[derive(Clone)]
71pub(crate) struct RuntimeExecutionTracing {
72 sink: Arc<dyn lash_trace::TraceSink>,
73 base_context: lash_trace::TraceContext,
74 scope_context: lash_trace::TraceContext,
75}
76
77impl RuntimeExecutionTracing {
78 pub(crate) fn new(
79 sink: Arc<dyn lash_trace::TraceSink>,
80 base_context: lash_trace::TraceContext,
81 scope_context: lash_trace::TraceContext,
82 ) -> Self {
83 Self {
84 sink,
85 base_context,
86 scope_context,
87 }
88 }
89
90 fn emit(&self, event: lash_trace::TraceEvent, clock: &dyn crate::Clock) {
91 crate::trace::emit_trace(
92 &Some(Arc::clone(&self.sink)),
93 &self.base_context,
94 self.scope_context.clone(),
95 event,
96 clock,
97 );
98 }
99}
100
101impl<'run> RuntimeExecutionContext<'run> {
102 pub(crate) fn drain_tool_trigger_outcomes(
103 &self,
104 ) -> Result<Vec<crate::tool_dispatch::ToolTriggerEffectOutcome>, crate::PluginError> {
105 self.dispatch
106 .trigger_outcomes
107 .drain()
108 .map_err(crate::PluginError::Session)
109 }
110
111 pub(super) fn process_scope(
112 &self,
113 parent_invocation: Option<crate::RuntimeInvocation>,
114 ) -> crate::ProcessOpScope<'_> {
115 crate::ProcessOpScope::new(self.dispatch.effect_controller.scoped())
116 .with_parent_invocation(parent_invocation)
117 .with_agent_frame_id(Some(self.dispatch.agent_frame_id.clone()))
118 }
119
120 #[allow(
121 clippy::too_many_arguments,
122 reason = "code execution bridge carries explicit per-turn runtime dependencies"
123 )]
124 pub(crate) fn new(
125 session_id: String,
126 dispatch: Arc<ToolDispatchContext<'run>>,
127 process_env_store: Arc<dyn crate::ProcessExecutionEnvStore>,
128 attachment_store: Arc<crate::SessionAttachmentStore>,
129 chronological_projection: Arc<crate::ChronologicalProjection>,
130 protocol_extension: Option<crate::ProtocolTurnExtensionHandle>,
131 turn_context: crate::TurnContext,
132 ) -> Self {
133 Self {
134 session_id,
135 dispatch,
136 process_env_store,
137 attachment_store,
138 chronological_projection,
139 protocol_extension,
140 turn_context,
141 execution_env_spec: crate::ProcessExecutionEnvSpec::new(
142 crate::PluginOptions::default(),
143 crate::SessionPolicy::default(),
144 ),
145 process_originator: None,
146 runtime_process_id: None,
147 process_event_context: None,
148 started_process_ids: Arc::default(),
149 process_env_ref: None,
150 process_wake_target: None,
151 parent_invocation: None,
152 turn_phase_probe: None,
153 turn_event_tx: None,
154 cancellation_token: None,
155 observe_turn_cancel: true,
156 tracing: None,
157 code_block_graph_key: None,
158 batch_parent_call_id: None,
159 process_work_driver: None,
160 }
161 }
162
163 pub fn session_id(&self) -> &str {
164 &self.session_id
165 }
166
167 pub fn execution_scope_id(&self) -> String {
168 self.dispatch
169 .effect_controller
170 .scoped()
171 .scope_id()
172 .to_string()
173 }
174
175 pub fn session_scope(&self) -> crate::SessionScope {
176 if self.dispatch.agent_frame_id.is_empty() {
177 crate::SessionScope::new(self.session_id.clone())
178 } else {
179 crate::SessionScope::for_agent_frame(
180 self.session_id.clone(),
181 self.dispatch.agent_frame_id.clone(),
182 )
183 }
184 }
185
186 pub fn trigger_store(&self) -> Option<Arc<dyn crate::TriggerStore>> {
187 self.dispatch
188 .trigger_router
189 .as_ref()
190 .map(crate::TriggerRouter::store)
191 }
192
193 pub fn trigger_registration_originator(&self) -> crate::ProcessOriginator {
194 self.process_originator
195 .clone()
196 .unwrap_or_else(|| crate::ProcessOriginator::session(self.session_scope()))
197 }
198
199 pub fn trigger_registration_wake_target(&self) -> Option<crate::SessionScope> {
200 self.process_wake_target
201 .clone()
202 .or_else(|| Some(self.session_scope()))
203 }
204
205 pub fn attachment_store(&self) -> Arc<crate::SessionAttachmentStore> {
206 Arc::clone(&self.attachment_store)
207 }
208
209 pub fn process_env_store(&self) -> Arc<dyn crate::ProcessExecutionEnvStore> {
210 Arc::clone(&self.process_env_store)
211 }
212
213 pub fn chronological_projection(&self) -> Arc<crate::ChronologicalProjection> {
214 Arc::clone(&self.chronological_projection)
215 }
216
217 pub fn protocol_extension<T: 'static>(&self) -> Option<&T> {
218 self.protocol_extension
219 .as_ref()
220 .and_then(|extension| extension.as_any().downcast_ref::<T>())
221 }
222
223 pub fn turn_context(&self) -> &crate::TurnContext {
224 &self.turn_context
225 }
226
227 pub fn tool_catalog(&self) -> Arc<crate::ToolCatalog> {
228 Arc::clone(&self.dispatch.tool_catalog)
229 }
230
231 pub(crate) fn session_graph_service(&self) -> &dyn crate::plugin::SessionGraphService {
232 self.dispatch.session_graph.as_ref()
233 }
234
235 pub(super) async fn emit_turn_activity(
236 &self,
237 correlation_id: TurnActivityId,
238 event: TurnEvent,
239 ) {
240 if let Some(tx) = &self.turn_event_tx {
241 let _ = tx.send(TurnActivity::new(correlation_id, event)).await;
242 }
243 }
244
245 pub(crate) fn with_turn_event_sender(mut self, turn_event_tx: Sender<TurnActivity>) -> Self {
246 self.turn_event_tx = Some(turn_event_tx);
247 self
248 }
249
250 pub(crate) fn with_tracing(mut self, tracing: Option<RuntimeExecutionTracing>) -> Self {
251 self.tracing = tracing;
252 self
253 }
254
255 pub(crate) fn with_code_block_graph_key(mut self, graph_key: Option<String>) -> Self {
256 self.code_block_graph_key = graph_key;
257 self
258 }
259
260 pub(crate) fn with_batch_parent_call_id(mut self, parent_call_id: Option<String>) -> Self {
261 self.batch_parent_call_id = parent_call_id;
262 self
263 }
264
265 pub(super) fn code_block_graph_key(&self) -> Option<String> {
268 self.code_block_graph_key.clone()
269 }
270
271 pub(super) fn batch_parent_call_id(&self) -> Option<String> {
274 self.batch_parent_call_id.clone()
275 }
276
277 pub(super) fn emit_tool_call_started_trace(
280 &self,
281 call_id: &str,
282 name: &str,
283 args: &serde_json::Value,
284 ) {
285 if let Some(tracing) = self.tracing.as_ref() {
286 tracing.emit(
287 lash_trace::TraceEvent::ToolCallStarted {
288 call_id: Some(call_id.to_string()),
289 name: name.to_string(),
290 args: args.clone(),
291 },
292 self.dispatch.clock.as_ref(),
293 );
294 }
295 }
296
297 pub(super) fn emit_tool_call_completed_trace(&self, record: &crate::ToolCallRecord) {
300 if let Some(tracing) = self.tracing.as_ref() {
301 tracing.emit(
302 lash_trace::TraceEvent::ToolCallCompleted {
303 call_id: record.call_id.clone(),
304 name: record.tool.clone(),
305 args: record.args.clone(),
306 output: crate::trace::trace_tool_call_output(&record.output),
307 duration_ms: record.duration_ms,
308 },
309 self.dispatch.clock.as_ref(),
310 );
311 }
312 }
313
314 pub(crate) fn with_parent_invocation(mut self, metadata: crate::RuntimeInvocation) -> Self {
315 self.parent_invocation = Some(metadata);
316 self
317 }
318
319 pub(crate) fn with_execution_env_spec(
320 mut self,
321 execution_env_spec: crate::ProcessExecutionEnvSpec,
322 ) -> Self {
323 self.execution_env_spec = execution_env_spec;
324 self
325 }
326
327 pub(crate) fn with_process_registration_context(
328 mut self,
329 registration: &crate::ProcessRegistration,
330 ) -> Self {
331 self.process_originator = Some(registration.provenance.originator.clone());
332 self.runtime_process_id = Some(registration.id.clone());
333 self.process_env_ref = registration.env_ref.clone();
334 self.process_wake_target = registration.wake_target.clone();
335 self
336 }
337
338 pub(crate) fn with_process_event_context(
339 mut self,
340 process_id: impl Into<String>,
341 registry: Arc<dyn crate::ProcessRegistry>,
342 awaiter: crate::ProcessAwaiter,
343 store: Option<Arc<dyn crate::RuntimePersistence>>,
344 session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
345 queued_work_driver: Option<crate::QueuedWorkDriver>,
346 ) -> Self {
347 self.process_event_context = Some(RuntimeExecutionProcessEventContext {
348 process_id: process_id.into(),
349 registry,
350 awaiter,
351 store,
352 session_store_factory,
353 queued_work_driver,
354 });
355 self
356 }
357
358 pub(super) fn record_started_process(&self, process_id: &str) {
362 self.started_process_ids
363 .lock()
364 .expect("started process ids lock")
365 .insert(process_id.to_string());
366 }
367
368 pub(super) fn is_run_local_process(&self, process_id: &str) -> bool {
369 self.started_process_ids
370 .lock()
371 .expect("started process ids lock")
372 .contains(process_id)
373 }
374
375 pub(crate) fn process_spawn_provenance(&self) -> Option<crate::ProcessSpawnProvenance> {
376 self.process_originator
377 .clone()
378 .map(|originator| crate::ProcessSpawnProvenance {
379 originator,
380 wake_target: self.process_wake_target.clone(),
381 })
382 }
383
384 pub(super) async fn attach_captured_process_execution_env(
385 &self,
386 registration: crate::ProcessRegistration,
387 ) -> Result<crate::ProcessRegistration, crate::PluginError> {
388 if registration.env_ref.is_some() {
389 return Ok(registration);
390 }
391 match registration.input.as_ref() {
392 crate::ProcessInput::ToolCall { .. } | crate::ProcessInput::Engine { .. } => {
393 let env_ref = self.captured_process_execution_env_ref().await?;
394 Ok(registration.with_execution_env_ref(Some(env_ref)))
395 }
396 crate::ProcessInput::External { .. } | crate::ProcessInput::SessionTurn { .. } => {
397 Ok(registration)
398 }
399 }
400 }
401
402 pub async fn captured_process_execution_env_ref(
403 &self,
404 ) -> Result<crate::ProcessExecutionEnvRef, crate::PluginError> {
405 if let Some(env_ref) = self.process_env_ref.clone() {
406 return Ok(env_ref);
407 }
408 crate::persist_process_execution_env(
409 self.process_env_store.as_ref(),
410 &self.execution_env_spec,
411 )
412 .await
413 }
414
415 pub(crate) fn with_turn_phase_probe(
416 mut self,
417 probe: Option<Arc<dyn crate::runtime::RuntimeTurnPhaseProbe>>,
418 ) -> Self {
419 self.turn_phase_probe = probe;
420 self
421 }
422
423 #[doc(hidden)]
424 pub fn named_phase(&self, phase: &'static str) -> crate::runtime::RuntimeNamedPhase {
425 crate::runtime::RuntimeNamedPhase::begin(self.turn_phase_probe.clone(), phase)
426 }
427
428 pub fn parent_invocation(&self) -> Option<&crate::RuntimeInvocation> {
429 self.parent_invocation.as_ref()
430 }
431
432 pub(crate) fn with_cancellation_token(mut self, cancellation_token: CancellationToken) -> Self {
433 self.cancellation_token = Some(cancellation_token);
434 self
435 }
436
437 pub(crate) fn without_turn_cancel_observation(mut self) -> Self {
438 self.observe_turn_cancel = false;
439 self
440 }
441
442 pub(crate) fn with_process_work_driver(
443 mut self,
444 process_work_driver: Option<crate::ProcessWorkDriver>,
445 ) -> Self {
446 self.process_work_driver = process_work_driver;
447 self
448 }
449
450 pub(crate) fn tool_scheduling(&self, name: &str) -> crate::ToolScheduling {
451 crate::tool_dispatch::resolve_tool_scheduling(&self.dispatch, name)
452 }
453
454 pub fn callable_tool_manifest(&self, name: &str) -> Option<crate::ToolManifest> {
455 crate::tool_dispatch::resolve_callable_manifest(&self.dispatch, name)
456 }
457
458 pub fn callable_tool_manifest_by_id(&self, id: &crate::ToolId) -> Option<crate::ToolManifest> {
459 crate::tool_dispatch::resolve_callable_manifest_by_id(&self.dispatch, id)
460 }
461
462 pub fn tool_argument_projection_policy(
463 &self,
464 name: &str,
465 ) -> crate::ToolArgumentProjectionPolicy {
466 crate::tool_dispatch::resolve_tool_argument_projection_policy(&self.dispatch, name)
467 }
468
469 pub async fn start_child_process(
470 &self,
471 registration: crate::ProcessRegistration,
472 kind: impl Into<String>,
473 label: Option<String>,
474 ) -> crate::ToolInvocationReply {
475 let _phase = self.named_phase("process.start_child");
476 let registration = match self
477 .attach_captured_process_execution_env(registration)
478 .await
479 {
480 Ok(registration) => registration,
481 Err(err) => {
482 return crate::ToolInvocationReply::error(serde_json::json!(err.to_string()));
483 }
484 };
485 let process_id = registration.id.clone();
486 let mut options = crate::ProcessStartOptions::new()
487 .with_descriptor(crate::ProcessHandleDescriptor::new(Some(kind), label));
488 if let Some(spawn) = self.process_spawn_provenance() {
489 options = options.with_spawn_provenance(spawn);
490 }
491 match self
492 .dispatch
493 .processes
494 .start(
495 &self.session_id,
496 registration,
497 options,
498 self.process_scope(self.parent_invocation.clone()),
499 )
500 .await
501 {
502 Ok(_) => {
503 self.record_started_process(&process_id);
504 crate::ToolInvocationReply::success(Self::process_handle_json(&process_id))
505 }
506 Err(err) => crate::ToolInvocationReply::error(serde_json::json!(err.to_string())),
507 }
508 }
509
510 pub async fn sleep_process(
511 &self,
512 scope: &str,
513 sequence: u64,
514 duration_ms: u64,
515 ) -> Result<(), crate::RuntimeEffectControllerError> {
516 let cancellation = self.cancellation_token.clone().unwrap_or_default();
517 let invocation = crate::runtime::causal::process_sleep_invocation(
518 &self.session_id,
519 self.parent_invocation.as_ref(),
520 scope,
521 sequence,
522 );
523 let outcome = self
524 .dispatch
525 .effect_controller
526 .controller()
527 .execute_effect(
528 crate::RuntimeEffectEnvelope::new(
529 invocation,
530 crate::RuntimeEffectCommand::Sleep { duration_ms },
531 ),
532 crate::RuntimeEffectLocalExecutor::sleep_with_clock(
533 cancellation,
534 std::sync::Arc::clone(&self.dispatch.clock),
535 )
536 .with_turn_cancel_observation(self.observe_turn_cancel),
537 )
538 .await?;
539 match outcome {
540 crate::RuntimeEffectOutcome::Sleep => Ok(()),
541 other => Err(crate::RuntimeEffectControllerError::new(
542 "runtime_effect_wrong_outcome",
543 format!("expected sleep outcome, got {}", other.kind().as_str()),
544 )),
545 }
546 }
547
548 pub async fn await_process_signal_event(
549 &self,
550 process_id: &str,
551 signal_name: &str,
552 event_ordinal: u64,
553 ) -> Result<serde_json::Value, crate::RuntimeEffectControllerError> {
554 let cancellation = self.cancellation_token.clone().unwrap_or_default();
555 let key = self
556 .dispatch
557 .effect_controller
558 .controller()
559 .await_event_key(
560 &crate::ExecutionScope::process(process_id),
561 crate::AwaitEventWaitIdentity::process_signal(
562 process_id,
563 signal_name,
564 event_ordinal,
565 ),
566 )
567 .await?;
568 let invocation = crate::runtime::causal::process_await_event_invocation(
569 &self.session_id,
570 self.parent_invocation.as_ref(),
571 process_id,
572 signal_name,
573 event_ordinal,
574 );
575 let outcome = self
576 .dispatch
577 .effect_controller
578 .controller()
579 .execute_effect(
580 crate::RuntimeEffectEnvelope::new(
581 invocation,
582 crate::RuntimeEffectCommand::AwaitEvent { key },
583 ),
584 crate::RuntimeEffectLocalExecutor::await_event_with_clock(
585 cancellation,
586 None,
587 std::sync::Arc::clone(&self.dispatch.clock),
588 )
589 .with_turn_cancel_observation(self.observe_turn_cancel),
590 )
591 .await?;
592 match outcome.into_await_event()? {
593 crate::Resolution::Ok(value) => Ok(value),
594 crate::Resolution::Err(err) => Err(crate::RuntimeEffectControllerError::new(
595 err.code,
596 err.message,
597 )),
598 crate::Resolution::Timeout => Err(crate::RuntimeEffectControllerError::new(
599 "process_signal_wait_timeout",
600 "process signal wait timed out",
601 )),
602 crate::Resolution::Cancelled => Err(crate::RuntimeEffectControllerError::new(
603 "process_signal_wait_cancelled",
604 "process signal wait was cancelled",
605 )),
606 }
607 }
608
609 pub async fn signal_process_by_id(
610 &self,
611 registry: Arc<dyn crate::ProcessRegistry>,
612 process_id: &str,
613 signal_name: &str,
614 signal_id: String,
615 payload: serde_json::Value,
616 ) -> Result<crate::ProcessEvent, crate::RuntimeEffectControllerError> {
617 let event_type = crate::process_signal_event_type(signal_name)?;
618 let replay_key = format!("process:{process_id}:signal.{signal_name}:{signal_id}");
619 let signal_payload = payload.clone();
620 let command = crate::ProcessCommand::Signal {
621 process_id: process_id.to_string(),
622 signal_name: signal_name.to_string(),
623 signal_id,
624 request: crate::ProcessEventAppendRequest::new(event_type.clone(), payload)
625 .with_replay_key(replay_key),
626 };
627 let effect_id = command.effect_id();
628 let invocation = crate::runtime::causal::process_effect_invocation(
629 &self.session_id,
630 self.parent_invocation.clone(),
631 &effect_id,
632 );
633 let outcome = self
634 .dispatch
635 .effect_controller
636 .controller()
637 .execute_effect(
638 crate::RuntimeEffectEnvelope::new(
639 invocation,
640 crate::RuntimeEffectCommand::process(command),
641 ),
642 crate::RuntimeEffectLocalExecutor::processes(
643 Arc::clone(®istry),
644 self.process_work_driver.clone(),
645 ),
646 )
647 .await?;
648 match outcome.into_process()? {
649 crate::ProcessEffectOutcome::Signal { event } => {
650 let waiting_ordinal =
651 registry
652 .get_process(process_id)
653 .await
654 .and_then(|record| match record.wait {
655 Some(crate::WaitState {
656 kind:
657 crate::WaitKind::Signal {
658 name,
659 event_type: wait_event_type,
660 ordinal,
661 ..
662 },
663 ..
664 }) if name == signal_name && wait_event_type == event_type => {
665 Some(ordinal)
666 }
667 _ => None,
668 });
669 let ordinal = match waiting_ordinal {
670 Some(ordinal) => ordinal,
671 None => {
672 registry
673 .count_events_through(process_id, &event_type, event.sequence)
674 .await?
675 }
676 };
677 if ordinal > 0 {
678 let key = self
679 .dispatch
680 .effect_controller
681 .controller()
682 .await_event_key(
683 &crate::ExecutionScope::process(process_id),
684 crate::AwaitEventWaitIdentity::process_signal(
685 process_id,
686 signal_name,
687 ordinal,
688 ),
689 )
690 .await?;
691 let _ = self
692 .dispatch
693 .effect_controller
694 .controller()
695 .resolve_await_event(&key, crate::Resolution::Ok(signal_payload))
696 .await?;
697 }
698 Ok(*event)
699 }
700 other => Err(crate::RuntimeEffectControllerError::new(
701 "runtime_effect_wrong_outcome",
702 format!("expected signal outcome, got {other:?}"),
703 )),
704 }
705 }
706
707 pub async fn append_process_event(
708 &self,
709 registry: Arc<dyn crate::ProcessRegistry>,
710 process_id: &str,
711 request: crate::ProcessEventAppendRequest,
712 ) -> Result<crate::ProcessEvent, crate::PluginError> {
713 let result = registry.append_event(process_id, request).await?;
714 if let Some(context) = self.process_event_context.as_ref() {
715 crate::tool_provider::process_events::enqueue_wake_delivery(
716 context.store.clone(),
717 context.session_store_factory.as_ref(),
718 result.wake_delivery,
719 Some(self.session_graph_service()),
720 context.queued_work_driver.as_ref(),
721 )
722 .await?;
723 }
724 Ok(result.event)
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731 use crate::tool_dispatch::ToolDispatchContext;
732 use crate::{ToolCall, ToolProvider, ToolResult};
733
734 struct NoopTools;
735
736 #[async_trait::async_trait]
737 impl ToolProvider for NoopTools {
738 fn tool_manifests(&self) -> Vec<crate::ToolManifest> {
739 Vec::new()
740 }
741
742 fn resolve_contract(&self, _name: &str) -> Option<Arc<crate::ToolContract>> {
743 None
744 }
745
746 async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
747 ToolResult::err_fmt("not used")
748 }
749 }
750
751 #[test]
752 fn tool_argument_projection_policy_resolves_from_active_catalog_and_defaults_unknown() {
753 let tool = crate::ToolDefinition::raw(
754 "tool:seedy",
755 "seedy",
756 "Seed-aware",
757 crate::ToolDefinition::default_input_schema(),
758 serde_json::json!({ "type": "string" }),
759 )
760 .with_argument_projection(
761 crate::ToolArgumentProjectionPolicy::preserve_projected_refs_in_field("seed"),
762 );
763 let plugins = crate::plugin::PluginHost::empty()
764 .build_session("session", None)
765 .expect("plugin session");
766 let (event_tx, _event_rx) = tokio::sync::mpsc::channel(1);
767 let dispatch = Arc::new(ToolDispatchContext {
768 plugins,
769 tools: Arc::new(NoopTools),
770 tool_catalog: Arc::new(crate::ToolCatalog::from_tools(
771 vec![tool.manifest()],
772 std::collections::BTreeMap::new(),
773 )),
774 sessions: Arc::new(crate::testing::MockSessionManager::default()),
775 session_lifecycle: Arc::new(crate::testing::MockSessionManager::default()),
776 session_graph: Arc::new(crate::testing::MockSessionManager::default()),
777 processes: Arc::new(crate::UnavailableProcessService),
778 process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
779 trigger_router: None,
780 effect_controller: crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
781 crate::InlineRuntimeEffectController,
782 )),
783 direct_completions: crate::DirectCompletionClient::unavailable(
784 "direct completions are unavailable in this test context",
785 ),
786 parent_invocation: None,
787 execution_env_spec: crate::ProcessExecutionEnvSpec::new(
788 crate::PluginOptions::default(),
789 crate::SessionPolicy::default(),
790 ),
791 session_id: "session".to_string(),
792 agent_frame_id: String::new(),
793 event_tx,
794 checkpoint_messages: crate::tool_dispatch::CheckpointMessageBuffer::default(),
795 trigger_outcomes: crate::tool_dispatch::ToolTriggerOutcomeBuffer::default(),
796 attachment_store: Arc::new(crate::SessionAttachmentStore::in_memory()),
797 turn_context: crate::TurnContext::default(),
798 clock: std::sync::Arc::new(crate::SystemClock),
799 });
800 let ctx = RuntimeExecutionContext::new(
801 "session".to_string(),
802 dispatch,
803 Arc::new(crate::InMemoryProcessExecutionEnvStore::new()),
804 Arc::new(crate::SessionAttachmentStore::in_memory()),
805 Arc::new(crate::ChronologicalProjection::default()),
806 None,
807 crate::TurnContext::default(),
808 );
809
810 assert_eq!(
811 ctx.tool_argument_projection_policy("seedy"),
812 crate::ToolArgumentProjectionPolicy::preserve_projected_refs_in_field("seed")
813 );
814 assert_eq!(
815 ctx.tool_argument_projection_policy("missing"),
816 crate::ToolArgumentProjectionPolicy::MaterializeProjectedValues
817 );
818 }
819}