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
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    /// Per-tool trace emission handle for this execution. Present only when the
30    /// host installed a trace sink; `None` keeps every trace call a no-op.
31    tracing: Option<RuntimeExecutionTracing>,
32    /// Graph key of the enclosing code block, stamped onto the per-tool
33    /// `TurnEvent`s emitted from this context so consumers can attribute a tool
34    /// call to its code block without ordering heuristics. `None` when the
35    /// context is not executing a code block.
36    code_block_graph_key: Option<String>,
37    /// Call id of the parent `batch` tool call when this context runs the
38    /// children of a batch dispatch, stamped onto child `TurnEvent`s. `None`
39    /// for top-level tool execution.
40    batch_parent_call_id: Option<String>,
41    /// Work-driver handle for this execution's process wiring, when the
42    /// deployment provides one. Threaded through so in-run process
43    /// operations (e.g. signalling another process) that build their own
44    /// `RuntimeEffectLocalExecutor::processes(..)` call can hand it along
45    /// instead of falling back to hub-less backoff polling.
46    process_work_driver: Option<crate::ProcessWorkDriver>,
47    /// Process ids started by THIS execution context. Possession of a handle
48    /// the run itself created is sufficient capability to await/cancel it —
49    /// run-local children are not session handle grants (the ephemeral
50    /// execution scope must never appear in durable grant state).
51    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/// Trace-sink handle threaded into tool execution so per-tool trace events are
65/// emitted from the single shared seam, whichever protocol drives the turn.
66///
67/// `scope_context` carries the turn-scoped identity (session / turn / iteration)
68/// so [`crate::trace::assign_span_identity`] stamps `tool:<call_id>` under the
69/// right turn; `base_context` carries the host's run-level trace context.
70#[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    /// Graph key of the enclosing code block for tool calls run from this
346    /// context, or `None` when no code block is executing.
347    pub(super) fn code_block_graph_key(&self) -> Option<String> {
348        self.code_block_graph_key.clone()
349    }
350
351    /// Parent batch call id for tool calls run from this context, or `None`
352    /// when this context is not executing batch children.
353    pub(super) fn batch_parent_call_id(&self) -> Option<String> {
354        self.batch_parent_call_id.clone()
355    }
356
357    /// Emit a `ToolCallStarted` trace event for a tool run from this context.
358    /// No-op when the host installed no trace sink.
359    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    /// Emit a `ToolCallCompleted` trace event for a tool run from this context.
378    /// No-op when the host installed no trace sink.
379    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    /// Spawn provenance for children started by this context, present only
439    /// when this context executes a process: children inherit the chain's
440    /// originator and wake target instead of the ephemeral execution scope.
441    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(&registry),
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}