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