1use std::sync::{Arc, Mutex};
2
3use lash_sansio::llm::types::ProviderReplayMeta;
4use serde::{Deserialize, Serialize};
5
6use crate::plugin::{
7 PluginError, SessionGraphService, SessionLifecycleService, SessionSnapshot, SessionStateService,
8};
9use crate::{ToolContract, ToolDefinition, ToolId, ToolManifest, ToolResult};
10
11mod attachments;
12mod direct_completion;
13mod dispatch;
14mod process;
15pub(crate) mod process_events;
16mod session;
17mod triggers;
18
19pub use attachments::ToolAttachmentClient;
20pub use direct_completion::ToolDirectCompletionClient;
21pub use dispatch::ToolDispatchClient;
22pub use process::ToolSessionProcessAdmin;
23pub use process_events::ToolProcessEventClient;
24pub use session::{ToolSessionAdmin, ToolSessionModel};
25pub use triggers::ToolTriggerClient;
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, Default)]
39pub(crate) struct ToolCompletionState {
40 key: Arc<Mutex<Option<crate::AwaitEventKey>>>,
41}
42
43impl ToolCompletionState {
44 fn store(
45 &self,
46 key: crate::AwaitEventKey,
47 ) -> Result<crate::AwaitEventKey, crate::RuntimeError> {
48 let mut guard = self.key.lock().map_err(|_| {
49 crate::RuntimeError::new(
50 "tool_completion_state_poisoned",
51 "tool completion key state lock poisoned",
52 )
53 })?;
54 if let Some(existing) = guard.as_ref() {
55 return Ok(existing.clone());
56 }
57 *guard = Some(key.clone());
58 Ok(key)
59 }
60
61 pub(crate) fn take(&self) -> Result<Option<crate::AwaitEventKey>, crate::RuntimeError> {
62 self.key.lock().map(|mut guard| guard.take()).map_err(|_| {
63 crate::RuntimeError::new(
64 "tool_completion_state_poisoned",
65 "tool completion key state lock poisoned",
66 )
67 })
68 }
69}
70
71#[derive(Clone)]
74pub struct ToolContext<'run> {
75 pub(crate) session_id: String,
76 pub(crate) agent_frame_id: crate::AgentFrameId,
77 pub(crate) sessions: Arc<dyn SessionStateService>,
78 pub(crate) session_lifecycle: Arc<dyn SessionLifecycleService>,
79 pub(crate) processes: Arc<dyn crate::ProcessService>,
80 pub(crate) process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
81 pub(crate) effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
82 pub(crate) runtime_dispatch: Option<Arc<crate::tool_dispatch::ToolDispatchContext<'run>>>,
83 pub(crate) runtime_execution_context: Option<crate::RuntimeExecutionContext<'run>>,
84 pub(crate) cancellation_token: Option<tokio_util::sync::CancellationToken>,
85 pub(crate) async_process_id: Option<String>,
86 pub(crate) runtime_process_id: Option<String>,
87 pub(crate) process_events: Option<ToolProcessEventContext>,
88 pub(crate) attachment_store: Arc<crate::SessionAttachmentStore>,
89 pub(crate) direct_completions: crate::DirectCompletionClient<'run>,
90 pub(crate) prepared_payload: serde_json::Value,
91 pub(crate) tool_execution_binding: serde_json::Value,
92 pub(crate) tool_call_id: Option<String>,
94 pub(crate) attempt_number: u32,
95 pub(crate) max_attempts: u32,
96 pub(crate) replay_key: Option<String>,
97 pub(crate) completion: ToolCompletionState,
98 pub(crate) parent_invocation: Option<crate::RuntimeInvocation>,
99 pub(crate) execution_env_spec: crate::ProcessExecutionEnvSpec,
100 pub(crate) child_execution_trace_hook: Option<ToolChildExecutionTraceHook>,
101}
102
103#[derive(Clone)]
104pub struct ToolChildProcessStarted {
105 pub process_id: String,
106 pub child_entry_name: Option<String>,
107}
108
109#[derive(Clone)]
110pub struct ToolChildExecutionTraceHook {
111 on_child_process_started: Arc<dyn Fn(ToolChildProcessStarted) + Send + Sync>,
112}
113
114impl ToolChildExecutionTraceHook {
115 pub fn new(
116 on_child_process_started: impl Fn(ToolChildProcessStarted) + Send + Sync + 'static,
117 ) -> Self {
118 Self {
119 on_child_process_started: Arc::new(on_child_process_started),
120 }
121 }
122
123 pub fn child_process_started(&self, event: ToolChildProcessStarted) {
124 (self.on_child_process_started)(event);
125 }
126}
127
128#[derive(Clone)]
129pub(crate) struct ToolProcessEventContext {
130 process_id: String,
131 registry: Arc<dyn crate::ProcessRegistry>,
132 awaiter: crate::ProcessAwaiter,
133 store: Option<Arc<dyn crate::RuntimePersistence>>,
134 session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
135 session_graph: Arc<dyn SessionGraphService>,
136 queued_work_driver: Option<crate::QueuedWorkDriver>,
137}
138
139pub(crate) struct ToolContextBuilder<'run> {
140 session_id: String,
141 agent_frame_id: crate::AgentFrameId,
142 sessions: Arc<dyn SessionStateService>,
143 session_lifecycle: Arc<dyn SessionLifecycleService>,
144 session_graph: Arc<dyn SessionGraphService>,
145 processes: Arc<dyn crate::ProcessService>,
146 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
147 effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
148 runtime_dispatch: Option<Arc<crate::tool_dispatch::ToolDispatchContext<'run>>>,
149 runtime_execution_context: Option<crate::RuntimeExecutionContext<'run>>,
150 cancellation_token: Option<tokio_util::sync::CancellationToken>,
151 async_process_id: Option<String>,
152 runtime_process_id: Option<String>,
153 process_events: Option<ToolProcessEventContext>,
154 attachment_store: Arc<crate::SessionAttachmentStore>,
155 direct_completions: crate::DirectCompletionClient<'run>,
156 prepared_payload: serde_json::Value,
157 tool_execution_binding: serde_json::Value,
158 tool_call_id: Option<String>,
159 completion: ToolCompletionState,
160 parent_invocation: Option<crate::RuntimeInvocation>,
161 execution_env_spec: crate::ProcessExecutionEnvSpec,
162 child_execution_trace_hook: Option<ToolChildExecutionTraceHook>,
163}
164
165impl<'run> ToolContextBuilder<'run> {
166 pub(crate) fn from_dispatch(
167 dispatch: Arc<crate::tool_dispatch::ToolDispatchContext<'run>>,
168 ) -> Self {
169 Self {
170 session_id: dispatch.session_id.clone(),
171 agent_frame_id: dispatch.agent_frame_id.clone(),
172 sessions: Arc::clone(&dispatch.sessions),
173 session_lifecycle: Arc::clone(&dispatch.session_lifecycle),
174 session_graph: Arc::clone(&dispatch.session_graph),
175 processes: Arc::clone(&dispatch.processes),
176 process_cancel_ability: Arc::clone(&dispatch.process_cancel_ability),
177 effect_controller: dispatch.effect_controller.clone(),
178 runtime_dispatch: Some(Arc::clone(&dispatch)),
179 runtime_execution_context: None,
180 cancellation_token: None,
181 async_process_id: None,
182 runtime_process_id: None,
183 process_events: None,
184 attachment_store: Arc::clone(&dispatch.attachment_store),
185 direct_completions: dispatch.direct_completions.clone(),
186 prepared_payload: serde_json::Value::Null,
187 tool_execution_binding: serde_json::Value::Null,
188 tool_call_id: None,
189 completion: ToolCompletionState::default(),
190 parent_invocation: dispatch.parent_invocation.clone(),
191 execution_env_spec: dispatch.execution_env_spec.clone(),
192 child_execution_trace_hook: None,
193 }
194 }
195
196 #[cfg(any(test, feature = "testing"))]
197 pub(crate) fn tool_call_id(mut self, tool_call_id: impl Into<Option<String>>) -> Self {
198 self.tool_call_id = tool_call_id.into();
199 self
200 }
201
202 pub(crate) fn prepared_call(mut self, call: &PreparedToolCall) -> Self {
203 self.tool_call_id = Some(call.call_id.clone());
204 self.prepared_payload = call.prepared_payload.clone();
205 self
206 }
207
208 #[cfg(test)]
209 pub(crate) fn tool_execution_binding(mut self, binding: serde_json::Value) -> Self {
210 self.tool_execution_binding = binding;
211 self
212 }
213
214 pub(crate) fn cancellation_token(
215 mut self,
216 cancellation_token: Option<tokio_util::sync::CancellationToken>,
217 ) -> Self {
218 self.cancellation_token = cancellation_token;
219 self
220 }
221
222 pub(crate) fn runtime_execution_context(
223 mut self,
224 context: crate::RuntimeExecutionContext<'run>,
225 ) -> Self {
226 self.runtime_execution_context = Some(context);
227 self
228 }
229
230 pub(crate) fn runtime_process_id(mut self, process_id: Option<String>) -> Self {
231 self.runtime_process_id = process_id;
232 self
233 }
234
235 pub(crate) fn async_process(
236 mut self,
237 process_id: impl Into<String>,
238 cancellation_token: tokio_util::sync::CancellationToken,
239 ) -> Self {
240 self.async_process_id = Some(process_id.into());
241 self.cancellation_token = Some(cancellation_token);
242 self
243 }
244
245 pub(crate) fn process_events(
246 mut self,
247 process_id: impl Into<String>,
248 registry: Arc<dyn crate::ProcessRegistry>,
249 awaiter: crate::ProcessAwaiter,
250 store: Option<Arc<dyn crate::RuntimePersistence>>,
251 session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
252 queued_work_driver: Option<crate::QueuedWorkDriver>,
253 ) -> Self {
254 self.process_events = Some(ToolProcessEventContext {
255 process_id: process_id.into(),
256 registry,
257 awaiter,
258 store,
259 session_store_factory,
260 session_graph: Arc::clone(&self.session_graph),
261 queued_work_driver,
262 });
263 self
264 }
265
266 pub(crate) fn parent_invocation(mut self, metadata: Option<crate::RuntimeInvocation>) -> Self {
267 self.parent_invocation = metadata;
268 self
269 }
270
271 pub(crate) fn child_execution_trace_hook(
272 mut self,
273 hook: Option<ToolChildExecutionTraceHook>,
274 ) -> Self {
275 self.child_execution_trace_hook = hook;
276 self
277 }
278
279 pub(crate) fn build(self) -> ToolContext<'run> {
280 ToolContext {
281 session_id: self.session_id,
282 agent_frame_id: self.agent_frame_id,
283 sessions: self.sessions,
284 session_lifecycle: self.session_lifecycle,
285 processes: self.processes,
286 process_cancel_ability: self.process_cancel_ability,
287 effect_controller: self.effect_controller,
288 runtime_dispatch: self.runtime_dispatch,
289 runtime_execution_context: self.runtime_execution_context,
290 cancellation_token: self.cancellation_token,
291 async_process_id: self.async_process_id,
292 runtime_process_id: self.runtime_process_id,
293 process_events: self.process_events,
294 attachment_store: self.attachment_store,
295 direct_completions: self.direct_completions,
296 prepared_payload: self.prepared_payload,
297 tool_execution_binding: self.tool_execution_binding,
298 tool_call_id: self.tool_call_id,
299 attempt_number: 1,
300 max_attempts: 1,
301 replay_key: None,
302 completion: self.completion,
303 parent_invocation: self.parent_invocation,
304 execution_env_spec: self.execution_env_spec,
305 child_execution_trace_hook: self.child_execution_trace_hook,
306 }
307 }
308}
309
310impl<'run> ToolContext<'run> {
311 pub(crate) fn replay_validation_trace(&self) -> Option<crate::RuntimeEffectReplayTrace> {
312 self.runtime_execution_context
313 .as_ref()
314 .and_then(crate::RuntimeExecutionContext::replay_validation_trace)
315 }
316
317 pub(crate) fn to_static(&self) -> Option<ToolContext<'static>> {
318 Some(ToolContext {
319 session_id: self.session_id.clone(),
320 agent_frame_id: self.agent_frame_id.clone(),
321 sessions: Arc::clone(&self.sessions),
322 session_lifecycle: Arc::clone(&self.session_lifecycle),
323 processes: Arc::clone(&self.processes),
324 process_cancel_ability: Arc::clone(&self.process_cancel_ability),
325 effect_controller: self.effect_controller.to_static()?,
326 runtime_dispatch: match self.runtime_dispatch.as_ref() {
327 Some(dispatch) => Some(Arc::new(dispatch.to_static()?)),
328 None => None,
329 },
330 runtime_execution_context: match self.runtime_execution_context.as_ref() {
331 Some(context) => Some(context.to_static()?),
332 None => None,
333 },
334 cancellation_token: self.cancellation_token.clone(),
335 async_process_id: self.async_process_id.clone(),
336 runtime_process_id: self.runtime_process_id.clone(),
337 process_events: self.process_events.clone(),
338 attachment_store: Arc::clone(&self.attachment_store),
339 direct_completions: self.direct_completions.to_static()?,
340 prepared_payload: self.prepared_payload.clone(),
341 tool_execution_binding: self.tool_execution_binding.clone(),
342 tool_call_id: self.tool_call_id.clone(),
343 attempt_number: self.attempt_number,
344 max_attempts: self.max_attempts,
345 replay_key: self.replay_key.clone(),
346 completion: self.completion.clone(),
347 parent_invocation: self.parent_invocation.clone(),
348 execution_env_spec: self.execution_env_spec.clone(),
349 child_execution_trace_hook: self.child_execution_trace_hook.clone(),
350 })
351 }
352
353 #[cfg(any(test, feature = "testing"))]
354 #[expect(
355 clippy::too_many_arguments,
356 reason = "testing constructor mirrors the sealed runtime tool context dependencies"
357 )]
358 pub(crate) fn builder(
359 session_id: String,
360 sessions: Arc<dyn SessionStateService>,
361 session_lifecycle: Arc<dyn SessionLifecycleService>,
362 session_graph: Arc<dyn SessionGraphService>,
363 processes: Arc<dyn crate::ProcessService>,
364 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
365 effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
366 attachment_store: Arc<crate::SessionAttachmentStore>,
367 direct_completions: crate::DirectCompletionClient<'run>,
368 ) -> ToolContextBuilder<'run> {
369 ToolContextBuilder {
370 session_id,
371 agent_frame_id: String::new(),
372 sessions,
373 session_lifecycle,
374 session_graph,
375 processes,
376 process_cancel_ability,
377 effect_controller,
378 runtime_dispatch: None,
379 runtime_execution_context: None,
380 cancellation_token: None,
381 async_process_id: None,
382 runtime_process_id: None,
383 process_events: None,
384 attachment_store,
385 direct_completions,
386 prepared_payload: serde_json::Value::Null,
387 tool_execution_binding: serde_json::Value::Null,
388 tool_call_id: None,
389 completion: ToolCompletionState::default(),
390 parent_invocation: None,
391 execution_env_spec: crate::ProcessExecutionEnvSpec::new(
392 crate::PluginOptions::default(),
393 crate::SessionPolicy::default(),
394 ),
395 child_execution_trace_hook: None,
396 }
397 }
398
399 pub(crate) fn from_dispatch(
400 dispatch: Arc<crate::tool_dispatch::ToolDispatchContext<'run>>,
401 ) -> ToolContextBuilder<'run> {
402 ToolContextBuilder::from_dispatch(dispatch)
403 }
404
405 pub fn session_id(&self) -> &str {
406 &self.session_id
407 }
408
409 pub fn agent_frame_id(&self) -> &str {
410 &self.agent_frame_id
411 }
412
413 pub fn sessions(&self) -> ToolSessionAdmin<'run> {
414 ToolSessionAdmin {
415 session_id: self.session_id.clone(),
416 sessions: Arc::clone(&self.sessions),
417 session_lifecycle: Arc::clone(&self.session_lifecycle),
418 effect_controller: self.effect_controller.clone(),
419 }
420 }
421
422 pub fn dispatch(&self) -> ToolDispatchClient<'run> {
423 ToolDispatchClient {
424 context: self.clone(),
425 }
426 }
427
428 pub fn triggers(&self) -> ToolTriggerClient<'run> {
429 ToolTriggerClient {
430 context: self.clone(),
431 }
432 }
433
434 pub fn processes(&self) -> ToolSessionProcessAdmin<'run> {
435 ToolSessionProcessAdmin {
436 session_id: self.session_id.clone(),
437 agent_frame_id: self.agent_frame_id.clone(),
438 processes: Arc::clone(&self.processes),
439 process_cancel_ability: Arc::clone(&self.process_cancel_ability),
440 effect_controller: self.effect_controller.clone(),
441 parent_invocation: self.parent_invocation.clone(),
442 tool_call_id: self.tool_call_id.clone(),
443 execution_env_spec: self.execution_env_spec.clone(),
444 }
445 }
446
447 pub fn emit_child_process_started(
448 &self,
449 process_id: impl Into<String>,
450 child_entry_name: Option<String>,
451 ) {
452 let Some(hook) = &self.child_execution_trace_hook else {
453 return;
454 };
455 hook.child_process_started(ToolChildProcessStarted {
456 process_id: process_id.into(),
457 child_entry_name,
458 });
459 }
460
461 pub fn direct_completions(&self) -> ToolDirectCompletionClient<'run> {
462 ToolDirectCompletionClient {
463 session_id: self.session_id.clone(),
464 tool_call_id: self.tool_call_id.clone(),
465 direct_completions: self.direct_completions.clone(),
466 parent_invocation: self.parent_invocation.clone(),
467 }
468 }
469
470 pub fn attachments(&self) -> ToolAttachmentClient {
471 ToolAttachmentClient {
472 store: Arc::clone(&self.attachment_store),
473 }
474 }
475
476 pub fn process_events(&self) -> ToolProcessEventClient {
477 ToolProcessEventClient {
478 context: self.process_events.clone(),
479 }
480 }
481
482 pub fn cancellation_token(&self) -> Option<&tokio_util::sync::CancellationToken> {
483 self.cancellation_token.as_ref()
484 }
485
486 #[doc(hidden)]
487 pub fn named_phase(&self, phase: &'static str) -> crate::runtime::RuntimeNamedPhase {
488 match self.runtime_execution_context.as_ref() {
489 Some(context) => context.named_phase(phase),
490 None => crate::runtime::RuntimeNamedPhase::begin(None, phase),
491 }
492 }
493
494 pub fn async_process_id(&self) -> Option<&str> {
495 self.async_process_id.as_deref()
496 }
497
498 pub fn runtime_process_id(&self) -> Option<&str> {
499 self.async_process_id
500 .as_deref()
501 .or(self.runtime_process_id.as_deref())
502 .or_else(|| {
503 self.process_events
504 .as_ref()
505 .map(|context| context.process_id.as_str())
506 })
507 }
508
509 pub fn tool_call_id(&self) -> Option<&str> {
510 self.tool_call_id.as_deref()
511 }
512
513 pub fn prepared_payload(&self) -> &serde_json::Value {
514 &self.prepared_payload
515 }
516
517 pub fn tool_execution_binding(&self) -> &serde_json::Value {
518 &self.tool_execution_binding
519 }
520
521 pub fn decode_prepared_payload<T>(&self) -> Result<T, serde_json::Error>
522 where
523 T: serde::de::DeserializeOwned,
524 {
525 serde_json::from_value(self.prepared_payload.clone())
526 }
527
528 pub fn attempt_number(&self) -> u32 {
529 self.attempt_number
530 }
531
532 pub fn max_attempts(&self) -> u32 {
533 self.max_attempts
534 }
535
536 pub fn replay_key(&self) -> Option<&str> {
537 self.replay_key.as_deref()
538 }
539
540 pub async fn completion_key(&self) -> Result<crate::AwaitEventKey, crate::RuntimeError> {
554 let tool_call_id = self.tool_call_id.clone().ok_or_else(|| {
555 crate::RuntimeError::new(
556 "tool_completion_key_missing_call_id",
557 "completion keys require a prepared tool call id",
558 )
559 })?;
560 let scoped = self.effect_controller.scoped();
561 if scoped.controller().durability_tier() == crate::DurabilityTier::Inline
562 && !scoped
563 .controller()
564 .allows_process_lifetime_completion_keys()
565 {
566 return Err(crate::RuntimeError::new(
567 "tool_completion_key_process_lifetime",
568 "completion keys on an Inline-tier host die with the current process; construct the InlineEffectHost with allow_process_lifetime_completion_keys() only for an explicitly single-process deployment",
569 ));
570 }
571 let key = scoped
572 .controller()
573 .await_event_key(
574 scoped.execution_scope(),
575 crate::AwaitEventWaitIdentity::tool_completion(tool_call_id),
576 )
577 .await?;
578 self.completion.store(key)
579 }
580
581 pub(crate) fn take_completion_key(
582 &self,
583 ) -> Result<Option<crate::AwaitEventKey>, crate::RuntimeError> {
584 self.completion.take()
585 }
586
587 pub fn with_async_process(
588 mut self,
589 process_id: impl Into<String>,
590 cancellation_token: tokio_util::sync::CancellationToken,
591 ) -> Self {
592 self.async_process_id = Some(process_id.into());
593 self.runtime_process_id = self.async_process_id.clone();
594 self.cancellation_token = Some(cancellation_token);
595 self
596 }
597
598 #[cfg(any(test, feature = "testing"))]
599 #[doc(hidden)]
600 pub fn with_process_events_for_testing(
601 mut self,
602 process_id: impl Into<String>,
603 registry: Arc<dyn crate::ProcessRegistry>,
604 ) -> Self {
605 let awaiter = crate::ProcessAwaiter::polling(Arc::clone(®istry));
606 self.process_events = Some(ToolProcessEventContext {
607 process_id: process_id.into(),
608 registry,
609 awaiter,
610 store: None,
611 session_store_factory: None,
612 session_graph: Arc::new(crate::plugin::NoopSessionManager),
613 queued_work_driver: None,
614 });
615 self
616 }
617
618 pub(crate) fn with_retry_context(
619 mut self,
620 tool_name: &str,
621 attempt_number: u32,
622 max_attempts: u32,
623 ) -> Self {
624 self.attempt_number = attempt_number.max(1);
625 self.max_attempts = max_attempts.max(1);
626 self.replay_key = self
627 .tool_call_id
628 .as_ref()
629 .map(|call_id| format!("lash-tool:{}:{call_id}:{tool_name}", self.session_id));
630 self
631 }
632
633 pub(crate) fn with_prepared_payload(mut self, payload: serde_json::Value) -> Self {
634 self.prepared_payload = payload;
635 self
636 }
637
638 pub(crate) fn with_tool_execution_binding(mut self, binding: serde_json::Value) -> Self {
639 self.tool_execution_binding = binding;
640 self
641 }
642
643 pub(crate) fn with_attempt_dispatch(
644 mut self,
645 dispatch: Arc<crate::tool_dispatch::ToolDispatchContext<'run>>,
646 parent_invocation: crate::RuntimeInvocation,
647 ) -> Self {
648 self.effect_controller = dispatch.effect_controller.clone();
649 self.runtime_dispatch = Some(dispatch);
650 self.parent_invocation = Some(parent_invocation);
651 self
652 }
653
654 #[cfg(any(test, feature = "testing"))]
657 #[doc(hidden)]
658 #[expect(
659 clippy::too_many_arguments,
660 reason = "test-only constructor mirrors the sealed runtime tool context"
661 )]
662 pub fn __for_testing(
663 session_id: String,
664 sessions: Arc<dyn SessionStateService>,
665 session_lifecycle: Arc<dyn SessionLifecycleService>,
666 session_graph: Arc<dyn SessionGraphService>,
667 processes: Arc<dyn crate::ProcessService>,
668 attachment_store: Arc<crate::SessionAttachmentStore>,
669 direct_completions: crate::DirectCompletionClient<'static>,
670 tool_call_id: Option<String>,
671 ) -> ToolContext<'static> {
672 ToolContext::builder(
673 session_id,
674 sessions,
675 session_lifecycle,
676 session_graph,
677 processes,
678 Arc::new(crate::DefaultProcessCancelAbility),
679 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
680 crate::InlineRuntimeEffectController::default()
681 .allow_process_lifetime_completion_keys(),
682 )),
683 attachment_store,
684 direct_completions,
685 )
686 .tool_call_id(tool_call_id)
687 .build()
688 }
689
690 #[cfg(any(test, feature = "testing"))]
694 #[doc(hidden)]
695 #[expect(
696 clippy::too_many_arguments,
697 reason = "test-only constructor mirrors the sealed runtime context"
698 )]
699 pub fn __for_testing_with_process_cancel_ability(
700 session_id: String,
701 sessions: Arc<dyn SessionStateService>,
702 session_lifecycle: Arc<dyn SessionLifecycleService>,
703 session_graph: Arc<dyn SessionGraphService>,
704 processes: Arc<dyn crate::ProcessService>,
705 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
706 attachment_store: Arc<crate::SessionAttachmentStore>,
707 direct_completions: crate::DirectCompletionClient<'static>,
708 tool_call_id: Option<String>,
709 ) -> ToolContext<'static> {
710 ToolContext::builder(
711 session_id,
712 sessions,
713 session_lifecycle,
714 session_graph,
715 processes,
716 process_cancel_ability,
717 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
718 crate::InlineRuntimeEffectController::default()
719 .allow_process_lifetime_completion_keys(),
720 )),
721 attachment_store,
722 direct_completions,
723 )
724 .tool_call_id(tool_call_id)
725 .build()
726 }
727}
728
729#[derive(Clone, Debug, Serialize, Deserialize)]
735pub struct PreparedToolCall {
736 pub call_id: String,
737 pub tool_id: ToolId,
738 pub tool_name: String,
739 pub args: serde_json::Value,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
741 pub replay: Option<ProviderReplayMeta>,
742 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
743 pub prepared_payload: serde_json::Value,
744}
745
746impl PreparedToolCall {
747 pub fn identity(tool_id: ToolId, call: crate::sansio::PendingToolCall) -> Self {
748 Self {
749 call_id: call.call_id,
750 tool_id,
751 tool_name: call.tool_name,
752 args: call.args,
753 replay: call.replay,
754 prepared_payload: serde_json::Value::Null,
755 }
756 }
757
758 pub fn from_parts(
759 call_id: impl Into<String>,
760 tool_id: impl Into<ToolId>,
761 tool_name: impl Into<String>,
762 args: serde_json::Value,
763 replay: Option<ProviderReplayMeta>,
764 prepared_payload: serde_json::Value,
765 ) -> Self {
766 Self {
767 call_id: call_id.into(),
768 tool_id: tool_id.into(),
769 tool_name: tool_name.into(),
770 args,
771 replay,
772 prepared_payload,
773 }
774 }
775}
776
777#[derive(Clone, Debug, Serialize, Deserialize)]
783pub struct PreparedToolBatchCall {
784 pub call: PreparedToolCall,
785 pub replay_suffix: String,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub execution_grant: Option<Box<ToolExecutionGrant>>,
788}
789
790#[derive(Clone, Debug, Serialize, Deserialize)]
795pub struct PreparedToolBatch {
796 pub batch_id: String,
797 pub calls: Vec<PreparedToolBatchCall>,
798}
799
800impl PreparedToolBatch {
801 pub fn new(batch_id: impl Into<String>, calls: Vec<PreparedToolCall>) -> Self {
802 let batch_id = batch_id.into();
803 let calls = calls
804 .into_iter()
805 .enumerate()
806 .map(|(index, call)| PreparedToolBatchCall {
807 replay_suffix: format!("child:{index}:{}", call.call_id),
808 call,
809 execution_grant: None,
810 })
811 .collect();
812 Self { batch_id, calls }
813 }
814
815 pub fn new_with_grants(
816 batch_id: impl Into<String>,
817 calls: Vec<(PreparedToolCall, Option<ToolExecutionGrant>)>,
818 ) -> Self {
819 let batch_id = batch_id.into();
820 let calls = calls
821 .into_iter()
822 .enumerate()
823 .map(|(index, (call, execution_grant))| PreparedToolBatchCall {
824 replay_suffix: format!("child:{index}:{}", call.call_id),
825 call,
826 execution_grant: execution_grant.map(Box::new),
827 })
828 .collect();
829 Self { batch_id, calls }
830 }
831
832 pub fn is_empty(&self) -> bool {
833 self.calls.is_empty()
834 }
835
836 pub fn len(&self) -> usize {
837 self.calls.len()
838 }
839}
840
841#[derive(Clone, Debug, Serialize, Deserialize)]
849pub struct ToolExecutionGrant {
850 pub manifest: ToolManifest,
852 pub contract: Box<ToolContract>,
855 pub source_id: Option<String>,
858 pub execution_binding: serde_json::Value,
860}
861
862impl ToolExecutionGrant {
863 pub fn new(manifest: ToolManifest, contract: ToolContract) -> Self {
864 Self {
865 manifest,
866 contract: Box::new(contract),
867 source_id: None,
868 execution_binding: serde_json::Value::Null,
869 }
870 }
871
872 pub fn from_definition(definition: ToolDefinition) -> Self {
873 Self::new(definition.manifest(), definition.contract())
874 }
875
876 pub fn with_source_id(mut self, source_id: impl Into<String>) -> Self {
877 self.source_id = Some(source_id.into());
878 self
879 }
880
881 pub fn with_execution_binding(mut self, execution_binding: serde_json::Value) -> Self {
882 self.execution_binding = execution_binding;
883 self
884 }
885}
886
887#[derive(Clone)]
888pub struct ToolPrepareContext {
889 session_id: String,
890 sessions: Arc<dyn SessionStateService>,
891 turn_context: crate::TurnContext,
892 tool_call_id: Option<String>,
893 tool_execution_binding: serde_json::Value,
894}
895
896impl ToolPrepareContext {
897 pub(crate) fn with_execution_binding(
898 session_id: String,
899 sessions: Arc<dyn SessionStateService>,
900 turn_context: crate::TurnContext,
901 tool_call_id: Option<String>,
902 tool_execution_binding: serde_json::Value,
903 ) -> Self {
904 Self {
905 session_id,
906 sessions,
907 turn_context,
908 tool_call_id,
909 tool_execution_binding,
910 }
911 }
912
913 pub fn session_id(&self) -> &str {
914 &self.session_id
915 }
916
917 pub fn tool_call_id(&self) -> Option<&str> {
918 self.tool_call_id.as_deref()
919 }
920
921 pub fn tool_execution_binding(&self) -> &serde_json::Value {
922 &self.tool_execution_binding
923 }
924
925 pub fn turn_context(&self) -> &crate::TurnContext {
926 &self.turn_context
927 }
928
929 pub fn plugin_input<T>(&self, plugin_id: &'static str) -> Option<&T>
930 where
931 T: 'static,
932 {
933 self.turn_context.plugin_input::<T>(plugin_id)
934 }
935
936 pub async fn session_snapshot(&self) -> Result<SessionSnapshot, PluginError> {
937 self.sessions.snapshot_session(&self.session_id).await
938 }
939
940 pub async fn tool_catalog(&self) -> Result<Vec<serde_json::Value>, PluginError> {
941 self.sessions.tool_catalog(&self.session_id).await
942 }
943
944 pub async fn shared_tool_catalog(
945 &self,
946 ) -> Result<std::sync::Arc<Vec<serde_json::Value>>, PluginError> {
947 self.sessions.shared_tool_catalog(&self.session_id).await
948 }
949}
950
951pub struct ToolPrepareCall<'a> {
953 pub tool_id: ToolId,
954 pub pending: crate::sansio::PendingToolCall,
955 pub context: &'a ToolPrepareContext,
956}
957
958pub struct ToolCall<'a> {
965 pub name: &'a str,
966 pub args: &'a serde_json::Value,
967 pub context: &'a ToolContext<'a>,
968 pub progress: Option<&'a ProgressSender>,
969}
970
971#[async_trait::async_trait]
979pub trait ToolProvider: Send + Sync + 'static {
980 fn tool_manifests(&self) -> Vec<ToolManifest>;
981 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
982 self.tool_manifests()
983 .into_iter()
984 .find(|manifest| manifest.name == name)
985 }
986 fn resolve_manifest_by_id(&self, id: &ToolId) -> Option<ToolManifest> {
987 self.tool_manifests()
988 .into_iter()
989 .find(|manifest| manifest.id == *id)
990 }
991 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>>;
992 fn resolve_contract_by_id(&self, id: &ToolId) -> Option<Arc<ToolContract>> {
993 let manifest = self.resolve_manifest_by_id(id)?;
994 self.resolve_contract(&manifest.name)
995 }
996 async fn prepare_tool_call(
997 &self,
998 call: ToolPrepareCall<'_>,
999 ) -> Result<PreparedToolCall, ToolResult> {
1000 Ok(PreparedToolCall::identity(call.tool_id, call.pending))
1001 }
1002 async fn prepare_granted_tool_call(
1003 &self,
1004 grant: &ToolExecutionGrant,
1005 call: ToolPrepareCall<'_>,
1006 ) -> Result<PreparedToolCall, ToolResult> {
1007 let _ = call;
1008 Err(ToolResult::err_fmt(format_args!(
1009 "Granted execution is unsupported for tool id `{}`",
1010 grant.manifest.id
1011 )))
1012 }
1013 async fn execute(&self, call: ToolCall<'_>) -> ToolResult;
1014 async fn execute_granted(
1015 &self,
1016 grant: &ToolExecutionGrant,
1017 args: &serde_json::Value,
1018 context: &ToolContext<'_>,
1019 progress: Option<&ProgressSender>,
1020 ) -> ToolResult {
1021 let _ = (args, context, progress);
1022 ToolResult::err_fmt(format_args!(
1023 "Granted execution is unsupported for tool id `{}`",
1024 grant.manifest.id
1025 ))
1026 }
1027 async fn execute_by_id(
1028 &self,
1029 tool_id: &ToolId,
1030 args: &serde_json::Value,
1031 context: &ToolContext<'_>,
1032 progress: Option<&ProgressSender>,
1033 ) -> ToolResult {
1034 let Some(manifest) = self.resolve_manifest_by_id(tool_id) else {
1035 return ToolResult::err_fmt(format!("Unknown tool id: {tool_id}"));
1036 };
1037 self.execute(ToolCall {
1038 name: &manifest.name,
1039 args,
1040 context,
1041 progress,
1042 })
1043 .await
1044 }
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050
1051 #[test]
1052 fn tool_context_builder_carries_call_payload_and_cancellation_state() {
1053 let cancellation = tokio_util::sync::CancellationToken::new();
1054 let prepared = PreparedToolCall::from_parts(
1055 "call-1",
1056 "tool:demo_tool",
1057 "demo_tool",
1058 serde_json::json!({ "input": true }),
1059 None,
1060 serde_json::json!({ "prepared": true }),
1061 );
1062
1063 let context = ToolContext::builder(
1064 "session-1".to_string(),
1065 Arc::new(crate::testing::MockSessionManager::default()),
1066 Arc::new(crate::testing::MockSessionManager::default()),
1067 Arc::new(crate::testing::MockSessionManager::default()),
1068 Arc::new(crate::UnavailableProcessService),
1069 Arc::new(crate::DefaultProcessCancelAbility),
1070 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
1071 crate::InlineRuntimeEffectController::default(),
1072 )),
1073 Arc::new(crate::SessionAttachmentStore::in_memory()),
1074 crate::DirectCompletionClient::unavailable(
1075 "direct completions are unavailable in this test context",
1076 ),
1077 )
1078 .prepared_call(&prepared)
1079 .cancellation_token(Some(cancellation.clone()))
1080 .async_process("process-1", cancellation.clone())
1081 .build();
1082
1083 assert_eq!(context.session_id(), "session-1");
1084 assert_eq!(context.tool_call_id(), Some("call-1"));
1085 assert_eq!(
1086 context.prepared_payload(),
1087 &serde_json::json!({ "prepared": true })
1088 );
1089 assert_eq!(context.async_process_id(), Some("process-1"));
1090 assert!(context.cancellation_token().is_some());
1091 }
1092
1093 #[tokio::test]
1094 async fn inline_completion_key_requires_process_lifetime_opt_in() {
1095 let prepared = PreparedToolCall::from_parts(
1096 "call-inline-risk",
1097 "tool:demo_tool",
1098 "demo_tool",
1099 serde_json::json!({}),
1100 None,
1101 serde_json::json!({}),
1102 );
1103 let context = ToolContext::builder(
1104 "session-inline-risk".to_string(),
1105 Arc::new(crate::testing::MockSessionManager::default()),
1106 Arc::new(crate::testing::MockSessionManager::default()),
1107 Arc::new(crate::testing::MockSessionManager::default()),
1108 Arc::new(crate::UnavailableProcessService),
1109 Arc::new(crate::DefaultProcessCancelAbility),
1110 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
1111 crate::InlineRuntimeEffectController::default(),
1112 )),
1113 Arc::new(crate::SessionAttachmentStore::in_memory()),
1114 crate::DirectCompletionClient::unavailable(
1115 "direct completions are unavailable in this test context",
1116 ),
1117 )
1118 .prepared_call(&prepared)
1119 .build();
1120
1121 let error = context
1122 .completion_key()
1123 .await
1124 .expect_err("Inline completion keys must refuse by default");
1125 assert_eq!(error.code.as_str(), "tool_completion_key_process_lifetime");
1126 assert!(error.message.contains("die with the current process"));
1127 assert!(
1128 error
1129 .message
1130 .contains("allow_process_lifetime_completion_keys")
1131 );
1132 }
1133}