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