1use std::sync::Arc;
2
3use lash_sansio::llm::types::ProviderReplayMeta;
4use serde::{Deserialize, Serialize};
5
6use crate::plugin::{
7 PluginError, SessionGraphService, SessionLifecycleService, SessionSnapshot, SessionStateService,
8};
9use crate::{AttachmentStore, ToolContract, ToolManifest, ToolResult};
10
11mod attachments;
12mod direct_completion;
13mod dispatch;
14mod host_events;
15mod process;
16pub(crate) mod process_events;
17mod session;
18
19pub use attachments::ToolAttachmentControl;
20pub use direct_completion::ToolDirectCompletionControl;
21pub use dispatch::ToolDispatchControl;
22pub use host_events::ToolHostEventControl;
23pub use process::ToolProcessControl;
24pub use process_events::ToolProcessEventControl;
25pub use session::{ToolSessionControl, ToolSessionModel};
26
27#[derive(Clone, Debug)]
29pub struct SandboxMessage {
30 pub text: String,
31 pub kind: String,
33}
34
35pub type ProgressSender = tokio::sync::mpsc::UnboundedSender<SandboxMessage>;
37
38#[derive(Clone)]
41pub struct ToolContext<'run> {
42 pub(crate) session_id: String,
43 pub(crate) agent_frame_id: crate::AgentFrameId,
44 pub(crate) sessions: Arc<dyn SessionStateService>,
45 pub(crate) session_lifecycle: Arc<dyn SessionLifecycleService>,
46 pub(crate) processes: Arc<dyn crate::ProcessService>,
47 pub(crate) process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
48 pub(crate) effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
49 pub(crate) runtime_dispatch: Option<Arc<crate::tool_dispatch::ToolDispatchContext<'run>>>,
50 pub(crate) cancellation_token: Option<tokio_util::sync::CancellationToken>,
51 pub(crate) async_process_id: Option<String>,
52 pub(crate) process_events: Option<ToolProcessEventContext>,
53 pub(crate) attachment_store: Arc<dyn AttachmentStore>,
54 pub(crate) direct_completions: crate::DirectCompletionClient<'run>,
55 pub(crate) prepared_payload: serde_json::Value,
56 pub(crate) tool_call_id: Option<String>,
58 pub(crate) attempt_number: u32,
59 pub(crate) max_attempts: u32,
60 pub(crate) replay_key: Option<String>,
61 pub(crate) parent_invocation: Option<crate::RuntimeInvocation>,
62 pub(crate) lashlang_execution_call_site: Option<ToolLashlangExecutionCallSite>,
63}
64
65#[derive(Clone)]
66pub struct ToolLashlangExecutionCallSite {
67 sink: Arc<dyn lash_trace::TraceSink>,
68 base_context: lash_trace::TraceContext,
69 identity: lash_trace::TraceLashlangExecutionIdentity,
70 parent_node_id: String,
71 occurrence: u64,
72}
73
74impl ToolLashlangExecutionCallSite {
75 pub fn new(
76 sink: Arc<dyn lash_trace::TraceSink>,
77 base_context: lash_trace::TraceContext,
78 identity: lash_trace::TraceLashlangExecutionIdentity,
79 parent_node_id: impl Into<String>,
80 occurrence: u64,
81 ) -> Self {
82 Self {
83 sink,
84 base_context,
85 identity,
86 parent_node_id: parent_node_id.into(),
87 occurrence,
88 }
89 }
90}
91
92#[derive(Clone)]
93pub(crate) struct ToolProcessEventContext {
94 process_id: String,
95 registry: Arc<dyn crate::ProcessRegistry>,
96 wake_target_scope: Option<crate::ProcessScope>,
97 store: Option<Arc<dyn crate::RuntimePersistence>>,
98 session_graph: Arc<dyn SessionGraphService>,
99 queued_work_poke: Option<crate::QueuedWorkPoke>,
100}
101
102pub(crate) struct ToolContextBuilder<'run> {
103 session_id: String,
104 agent_frame_id: crate::AgentFrameId,
105 sessions: Arc<dyn SessionStateService>,
106 session_lifecycle: Arc<dyn SessionLifecycleService>,
107 session_graph: Arc<dyn SessionGraphService>,
108 processes: Arc<dyn crate::ProcessService>,
109 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
110 effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
111 runtime_dispatch: Option<Arc<crate::tool_dispatch::ToolDispatchContext<'run>>>,
112 cancellation_token: Option<tokio_util::sync::CancellationToken>,
113 async_process_id: Option<String>,
114 process_events: Option<ToolProcessEventContext>,
115 attachment_store: Arc<dyn AttachmentStore>,
116 direct_completions: crate::DirectCompletionClient<'run>,
117 prepared_payload: serde_json::Value,
118 tool_call_id: Option<String>,
119 parent_invocation: Option<crate::RuntimeInvocation>,
120 lashlang_execution_call_site: Option<ToolLashlangExecutionCallSite>,
121}
122
123impl<'run> ToolContextBuilder<'run> {
124 pub(crate) fn from_dispatch(
125 dispatch: Arc<crate::tool_dispatch::ToolDispatchContext<'run>>,
126 ) -> Self {
127 Self {
128 session_id: dispatch.session_id.clone(),
129 agent_frame_id: dispatch.agent_frame_id.clone(),
130 sessions: Arc::clone(&dispatch.sessions),
131 session_lifecycle: Arc::clone(&dispatch.session_lifecycle),
132 session_graph: Arc::clone(&dispatch.session_graph),
133 processes: Arc::clone(&dispatch.processes),
134 process_cancel_ability: Arc::clone(&dispatch.process_cancel_ability),
135 effect_controller: dispatch.effect_controller.clone(),
136 runtime_dispatch: Some(Arc::clone(&dispatch)),
137 cancellation_token: None,
138 async_process_id: None,
139 process_events: None,
140 attachment_store: Arc::clone(&dispatch.attachment_store),
141 direct_completions: dispatch.direct_completions.clone(),
142 prepared_payload: serde_json::Value::Null,
143 tool_call_id: None,
144 parent_invocation: dispatch.parent_invocation.clone(),
145 lashlang_execution_call_site: None,
146 }
147 }
148
149 #[cfg(any(test, feature = "testing"))]
150 pub(crate) fn tool_call_id(mut self, tool_call_id: impl Into<Option<String>>) -> Self {
151 self.tool_call_id = tool_call_id.into();
152 self
153 }
154
155 pub(crate) fn prepared_call(mut self, call: &PreparedToolCall) -> Self {
156 self.tool_call_id = Some(call.call_id.clone());
157 self.prepared_payload = call.prepared_payload.clone();
158 self
159 }
160
161 pub(crate) fn cancellation_token(
162 mut self,
163 cancellation_token: Option<tokio_util::sync::CancellationToken>,
164 ) -> Self {
165 self.cancellation_token = cancellation_token;
166 self
167 }
168
169 pub(crate) fn async_process(
170 mut self,
171 process_id: impl Into<String>,
172 cancellation_token: tokio_util::sync::CancellationToken,
173 ) -> Self {
174 self.async_process_id = Some(process_id.into());
175 self.cancellation_token = Some(cancellation_token);
176 self
177 }
178
179 pub(crate) fn process_events(
180 mut self,
181 process_id: impl Into<String>,
182 registry: Arc<dyn crate::ProcessRegistry>,
183 wake_target_scope: Option<crate::ProcessScope>,
184 store: Option<Arc<dyn crate::RuntimePersistence>>,
185 queued_work_poke: Option<crate::QueuedWorkPoke>,
186 ) -> Self {
187 self.process_events = Some(ToolProcessEventContext {
188 process_id: process_id.into(),
189 registry,
190 wake_target_scope,
191 store,
192 session_graph: Arc::clone(&self.session_graph),
193 queued_work_poke,
194 });
195 self
196 }
197
198 pub(crate) fn parent_invocation(mut self, metadata: Option<crate::RuntimeInvocation>) -> Self {
199 self.parent_invocation = metadata;
200 self
201 }
202
203 pub(crate) fn lashlang_execution_call_site(
204 mut self,
205 call_site: Option<ToolLashlangExecutionCallSite>,
206 ) -> Self {
207 self.lashlang_execution_call_site = call_site;
208 self
209 }
210
211 pub(crate) fn build(self) -> ToolContext<'run> {
212 ToolContext {
213 session_id: self.session_id,
214 agent_frame_id: self.agent_frame_id,
215 sessions: self.sessions,
216 session_lifecycle: self.session_lifecycle,
217 processes: self.processes,
218 process_cancel_ability: self.process_cancel_ability,
219 effect_controller: self.effect_controller,
220 runtime_dispatch: self.runtime_dispatch,
221 cancellation_token: self.cancellation_token,
222 async_process_id: self.async_process_id,
223 process_events: self.process_events,
224 attachment_store: self.attachment_store,
225 direct_completions: self.direct_completions,
226 prepared_payload: self.prepared_payload,
227 tool_call_id: self.tool_call_id,
228 attempt_number: 1,
229 max_attempts: 1,
230 replay_key: None,
231 parent_invocation: self.parent_invocation,
232 lashlang_execution_call_site: self.lashlang_execution_call_site,
233 }
234 }
235}
236
237impl<'run> ToolContext<'run> {
238 #[cfg(any(test, feature = "testing"))]
239 #[expect(
240 clippy::too_many_arguments,
241 reason = "testing constructor mirrors the sealed runtime tool context dependencies"
242 )]
243 pub(crate) fn builder(
244 session_id: String,
245 sessions: Arc<dyn SessionStateService>,
246 session_lifecycle: Arc<dyn SessionLifecycleService>,
247 session_graph: Arc<dyn SessionGraphService>,
248 processes: Arc<dyn crate::ProcessService>,
249 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
250 effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
251 attachment_store: Arc<dyn AttachmentStore>,
252 direct_completions: crate::DirectCompletionClient<'run>,
253 ) -> ToolContextBuilder<'run> {
254 ToolContextBuilder {
255 session_id,
256 agent_frame_id: String::new(),
257 sessions,
258 session_lifecycle,
259 session_graph,
260 processes,
261 process_cancel_ability,
262 effect_controller,
263 runtime_dispatch: None,
264 cancellation_token: None,
265 async_process_id: None,
266 process_events: None,
267 attachment_store,
268 direct_completions,
269 prepared_payload: serde_json::Value::Null,
270 tool_call_id: None,
271 parent_invocation: None,
272 lashlang_execution_call_site: None,
273 }
274 }
275
276 pub(crate) fn from_dispatch(
277 dispatch: Arc<crate::tool_dispatch::ToolDispatchContext<'run>>,
278 ) -> ToolContextBuilder<'run> {
279 ToolContextBuilder::from_dispatch(dispatch)
280 }
281
282 pub fn session_id(&self) -> &str {
283 &self.session_id
284 }
285
286 pub fn agent_frame_id(&self) -> &str {
287 &self.agent_frame_id
288 }
289
290 pub fn sessions(&self) -> ToolSessionControl<'run> {
291 ToolSessionControl {
292 session_id: self.session_id.clone(),
293 sessions: Arc::clone(&self.sessions),
294 session_lifecycle: Arc::clone(&self.session_lifecycle),
295 effect_controller: self.effect_controller.clone(),
296 }
297 }
298
299 pub fn dispatch(&self) -> ToolDispatchControl<'run> {
300 ToolDispatchControl {
301 context: self.clone(),
302 }
303 }
304
305 pub fn host_events(&self) -> ToolHostEventControl<'run> {
306 ToolHostEventControl {
307 context: self.clone(),
308 }
309 }
310
311 pub fn processes(&self) -> ToolProcessControl<'run> {
312 ToolProcessControl {
313 session_id: self.session_id.clone(),
314 agent_frame_id: self.agent_frame_id.clone(),
315 processes: Arc::clone(&self.processes),
316 process_cancel_ability: Arc::clone(&self.process_cancel_ability),
317 effect_controller: self.effect_controller.clone(),
318 parent_invocation: self.parent_invocation.clone(),
319 tool_call_id: self.tool_call_id.clone(),
320 }
321 }
322
323 pub fn emit_lashlang_child_process_started(
324 &self,
325 process_id: impl Into<String>,
326 child_entry_name: Option<String>,
327 ) {
328 let Some(call_site) = &self.lashlang_execution_call_site else {
329 return;
330 };
331 let child = lash_trace::TraceLashlangChildExecution {
332 scope: call_site.identity.scope.clone(),
333 subject: lash_trace::TraceRuntimeSubject::Process {
334 process_id: process_id.into(),
335 },
336 module_ref: None,
337 entry_ref: None,
338 entry_name: child_entry_name,
339 };
340 let child_graph_key = child.graph_key();
341 let event = lash_trace::TraceLashlangExecutionEvent::ChildStarted {
342 event_key: format!(
343 "lashlang_execution:{}:child:{}:{}:{}",
344 call_site.identity.graph_key(),
345 call_site.parent_node_id,
346 call_site.occurrence,
347 child_graph_key
348 ),
349 identity: call_site.identity.clone(),
350 parent_node_id: call_site.parent_node_id.clone(),
351 occurrence: call_site.occurrence,
352 child,
353 };
354 let mut context = lash_trace::TraceContext::default()
355 .for_session(call_site.identity.scope.session_id.clone());
356 if let Some(turn_id) = &call_site.identity.scope.turn_id {
357 context = context.for_turn(turn_id.clone());
358 }
359 if let Some(turn_index) = call_site.identity.scope.turn_index {
360 context = context.for_turn_index(turn_index);
361 }
362 if let Some(protocol_iteration) = call_site.identity.scope.protocol_iteration {
363 context = context.for_protocol_iteration(protocol_iteration);
364 }
365 if let lash_trace::TraceRuntimeSubject::Effect { effect_id, .. } =
366 &call_site.identity.subject
367 {
368 context.effect_id = Some(effect_id.clone());
369 }
370 crate::trace::emit_trace(
371 &Some(Arc::clone(&call_site.sink)),
372 &call_site.base_context,
373 context,
374 lash_trace::TraceEvent::LashlangExecution { event },
375 );
376 }
377
378 pub fn direct_completions(&self) -> ToolDirectCompletionControl<'run> {
379 ToolDirectCompletionControl {
380 session_id: self.session_id.clone(),
381 tool_call_id: self.tool_call_id.clone(),
382 direct_completions: self.direct_completions.clone(),
383 }
384 }
385
386 pub fn attachments(&self) -> ToolAttachmentControl {
387 ToolAttachmentControl {
388 store: Arc::clone(&self.attachment_store),
389 }
390 }
391
392 pub fn process_events(&self) -> ToolProcessEventControl {
393 ToolProcessEventControl {
394 context: self.process_events.clone(),
395 }
396 }
397
398 pub fn cancellation_token(&self) -> Option<&tokio_util::sync::CancellationToken> {
399 self.cancellation_token.as_ref()
400 }
401
402 pub fn async_process_id(&self) -> Option<&str> {
403 self.async_process_id.as_deref()
404 }
405
406 pub fn tool_call_id(&self) -> Option<&str> {
407 self.tool_call_id.as_deref()
408 }
409
410 pub fn prepared_payload(&self) -> &serde_json::Value {
411 &self.prepared_payload
412 }
413
414 pub fn decode_prepared_payload<T>(&self) -> Result<T, serde_json::Error>
415 where
416 T: serde::de::DeserializeOwned,
417 {
418 serde_json::from_value(self.prepared_payload.clone())
419 }
420
421 pub fn attempt_number(&self) -> u32 {
422 self.attempt_number
423 }
424
425 pub fn max_attempts(&self) -> u32 {
426 self.max_attempts
427 }
428
429 pub fn replay_key(&self) -> Option<&str> {
430 self.replay_key.as_deref()
431 }
432
433 pub fn with_async_process(
434 mut self,
435 process_id: impl Into<String>,
436 cancellation_token: tokio_util::sync::CancellationToken,
437 ) -> Self {
438 self.async_process_id = Some(process_id.into());
439 self.cancellation_token = Some(cancellation_token);
440 self
441 }
442
443 #[cfg(any(test, feature = "testing"))]
444 #[doc(hidden)]
445 pub fn with_process_events_for_testing(
446 mut self,
447 process_id: impl Into<String>,
448 registry: Arc<dyn crate::ProcessRegistry>,
449 ) -> Self {
450 self.process_events = Some(ToolProcessEventContext {
451 process_id: process_id.into(),
452 registry,
453 wake_target_scope: None,
454 store: None,
455 session_graph: Arc::new(crate::plugin::NoopSessionManager),
456 queued_work_poke: None,
457 });
458 self
459 }
460
461 pub(crate) fn with_retry_context(
462 mut self,
463 tool_name: &str,
464 attempt_number: u32,
465 max_attempts: u32,
466 ) -> Self {
467 self.attempt_number = attempt_number.max(1);
468 self.max_attempts = max_attempts.max(1);
469 self.replay_key = self
470 .tool_call_id
471 .as_ref()
472 .map(|call_id| format!("lash-tool:{}:{call_id}:{tool_name}", self.session_id));
473 self
474 }
475
476 pub(crate) fn with_prepared_payload(mut self, payload: serde_json::Value) -> Self {
477 self.prepared_payload = payload;
478 self
479 }
480
481 #[cfg(any(test, feature = "testing"))]
484 #[doc(hidden)]
485 #[expect(
486 clippy::too_many_arguments,
487 reason = "test-only constructor mirrors the sealed runtime tool context"
488 )]
489 pub fn __for_testing(
490 session_id: String,
491 sessions: Arc<dyn SessionStateService>,
492 session_lifecycle: Arc<dyn SessionLifecycleService>,
493 session_graph: Arc<dyn SessionGraphService>,
494 processes: Arc<dyn crate::ProcessService>,
495 attachment_store: Arc<dyn AttachmentStore>,
496 direct_completions: crate::DirectCompletionClient<'static>,
497 tool_call_id: Option<String>,
498 ) -> ToolContext<'static> {
499 ToolContext::builder(
500 session_id,
501 sessions,
502 session_lifecycle,
503 session_graph,
504 processes,
505 Arc::new(crate::DefaultProcessCancelAbility),
506 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
507 crate::InlineRuntimeEffectController,
508 )),
509 attachment_store,
510 direct_completions,
511 )
512 .tool_call_id(tool_call_id)
513 .build()
514 }
515
516 #[cfg(any(test, feature = "testing"))]
520 #[doc(hidden)]
521 #[expect(
522 clippy::too_many_arguments,
523 reason = "test-only constructor mirrors the sealed runtime context"
524 )]
525 pub fn __for_testing_with_process_cancel_ability(
526 session_id: String,
527 sessions: Arc<dyn SessionStateService>,
528 session_lifecycle: Arc<dyn SessionLifecycleService>,
529 session_graph: Arc<dyn SessionGraphService>,
530 processes: Arc<dyn crate::ProcessService>,
531 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
532 attachment_store: Arc<dyn AttachmentStore>,
533 direct_completions: crate::DirectCompletionClient<'static>,
534 tool_call_id: Option<String>,
535 ) -> ToolContext<'static> {
536 ToolContext::builder(
537 session_id,
538 sessions,
539 session_lifecycle,
540 session_graph,
541 processes,
542 process_cancel_ability,
543 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
544 crate::InlineRuntimeEffectController,
545 )),
546 attachment_store,
547 direct_completions,
548 )
549 .tool_call_id(tool_call_id)
550 .build()
551 }
552}
553
554#[derive(Clone, Debug, Serialize, Deserialize)]
560pub struct PreparedToolCall {
561 pub call_id: String,
562 pub tool_name: String,
563 pub args: serde_json::Value,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
565 pub replay: Option<ProviderReplayMeta>,
566 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
567 pub prepared_payload: serde_json::Value,
568}
569
570impl PreparedToolCall {
571 pub fn identity(call: crate::sansio::PendingToolCall) -> Self {
572 Self {
573 call_id: call.call_id,
574 tool_name: call.tool_name,
575 args: call.args,
576 replay: call.replay,
577 prepared_payload: serde_json::Value::Null,
578 }
579 }
580
581 pub fn from_parts(
582 call_id: impl Into<String>,
583 tool_name: impl Into<String>,
584 args: serde_json::Value,
585 replay: Option<ProviderReplayMeta>,
586 prepared_payload: serde_json::Value,
587 ) -> Self {
588 Self {
589 call_id: call_id.into(),
590 tool_name: tool_name.into(),
591 args,
592 replay,
593 prepared_payload,
594 }
595 }
596}
597
598#[derive(Clone)]
599pub struct ToolPrepareContext {
600 session_id: String,
601 sessions: Arc<dyn SessionStateService>,
602 turn_context: crate::TurnContext,
603 tool_call_id: Option<String>,
604}
605
606impl ToolPrepareContext {
607 pub(crate) fn new(
608 session_id: String,
609 sessions: Arc<dyn SessionStateService>,
610 turn_context: crate::TurnContext,
611 tool_call_id: Option<String>,
612 ) -> Self {
613 Self {
614 session_id,
615 sessions,
616 turn_context,
617 tool_call_id,
618 }
619 }
620
621 pub fn session_id(&self) -> &str {
622 &self.session_id
623 }
624
625 pub fn tool_call_id(&self) -> Option<&str> {
626 self.tool_call_id.as_deref()
627 }
628
629 pub fn turn_context(&self) -> &crate::TurnContext {
630 &self.turn_context
631 }
632
633 pub fn plugin_input<T>(&self, plugin_id: &'static str) -> Option<&T>
634 where
635 T: 'static,
636 {
637 self.turn_context.plugin_input::<T>(plugin_id)
638 }
639
640 pub async fn session_snapshot(&self) -> Result<SessionSnapshot, PluginError> {
641 self.sessions.snapshot_session(&self.session_id).await
642 }
643
644 pub async fn tool_catalog(&self) -> Result<Vec<serde_json::Value>, PluginError> {
645 self.sessions.tool_catalog(&self.session_id).await
646 }
647
648 pub async fn shared_tool_catalog(
649 &self,
650 ) -> Result<std::sync::Arc<Vec<serde_json::Value>>, PluginError> {
651 self.sessions.shared_tool_catalog(&self.session_id).await
652 }
653}
654
655pub struct ToolPrepareCall<'a> {
657 pub pending: crate::sansio::PendingToolCall,
658 pub context: &'a ToolPrepareContext,
659}
660
661pub struct ToolCall<'a> {
668 pub name: &'a str,
669 pub args: &'a serde_json::Value,
670 pub context: &'a ToolContext<'a>,
671 pub progress: Option<&'a ProgressSender>,
672}
673
674#[async_trait::async_trait]
682pub trait ToolProvider: Send + Sync + 'static {
683 fn tool_manifests(&self) -> Vec<ToolManifest>;
684 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
685 self.tool_manifests()
686 .into_iter()
687 .find(|manifest| manifest.name == name)
688 }
689 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>>;
690 async fn prepare_tool_call(
691 &self,
692 call: ToolPrepareCall<'_>,
693 ) -> Result<PreparedToolCall, ToolResult> {
694 Ok(PreparedToolCall::identity(call.pending))
695 }
696 async fn execute(&self, call: ToolCall<'_>) -> ToolResult;
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn tool_context_builder_carries_call_payload_and_cancellation_state() {
705 let cancellation = tokio_util::sync::CancellationToken::new();
706 let prepared = PreparedToolCall::from_parts(
707 "call-1",
708 "demo_tool",
709 serde_json::json!({ "input": true }),
710 None,
711 serde_json::json!({ "prepared": true }),
712 );
713
714 let context = ToolContext::builder(
715 "session-1".to_string(),
716 Arc::new(crate::testing::MockSessionManager::default()),
717 Arc::new(crate::testing::MockSessionManager::default()),
718 Arc::new(crate::testing::MockSessionManager::default()),
719 Arc::new(crate::UnavailableProcessService),
720 Arc::new(crate::DefaultProcessCancelAbility),
721 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
722 crate::InlineRuntimeEffectController,
723 )),
724 Arc::new(crate::InMemoryAttachmentStore::new()),
725 crate::DirectCompletionClient::unavailable(
726 "direct completions are unavailable in this test context",
727 ),
728 )
729 .prepared_call(&prepared)
730 .cancellation_token(Some(cancellation.clone()))
731 .async_process("process-1", cancellation.clone())
732 .build();
733
734 assert_eq!(context.session_id(), "session-1");
735 assert_eq!(context.tool_call_id(), Some("call-1"));
736 assert_eq!(
737 context.prepared_payload(),
738 &serde_json::json!({ "prepared": true })
739 );
740 assert_eq!(context.async_process_id(), Some("process-1"));
741 assert!(context.cancellation_token().is_some());
742 }
743}