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    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    /// Graph key of the enclosing code block for tool calls run from this
266    /// context, or `None` when no code block is executing.
267    pub(super) fn code_block_graph_key(&self) -> Option<String> {
268        self.code_block_graph_key.clone()
269    }
270
271    /// Parent batch call id for tool calls run from this context, or `None`
272    /// when this context is not executing batch children.
273    pub(super) fn batch_parent_call_id(&self) -> Option<String> {
274        self.batch_parent_call_id.clone()
275    }
276
277    /// Emit a `ToolCallStarted` trace event for a tool run from this context.
278    /// No-op when the host installed no trace sink.
279    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    /// Emit a `ToolCallCompleted` trace event for a tool run from this context.
298    /// No-op when the host installed no trace sink.
299    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    /// Spawn provenance for children started by this context, present only
359    /// when this context executes a process: children inherit the chain's
360    /// originator and wake target instead of the ephemeral execution scope.
361    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(&registry),
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}