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 pub(crate) fn tool_execution_binding(mut self, binding: serde_json::Value) -> Self {
209 self.tool_execution_binding = binding;
210 self
211 }
212
213 pub(crate) fn cancellation_token(
214 mut self,
215 cancellation_token: Option<tokio_util::sync::CancellationToken>,
216 ) -> Self {
217 self.cancellation_token = cancellation_token;
218 self
219 }
220
221 pub(crate) fn runtime_execution_context(
222 mut self,
223 context: crate::RuntimeExecutionContext<'run>,
224 ) -> Self {
225 self.runtime_execution_context = Some(context);
226 self
227 }
228
229 pub(crate) fn runtime_process_id(mut self, process_id: Option<String>) -> Self {
230 self.runtime_process_id = process_id;
231 self
232 }
233
234 pub(crate) fn async_process(
235 mut self,
236 process_id: impl Into<String>,
237 cancellation_token: tokio_util::sync::CancellationToken,
238 ) -> Self {
239 self.async_process_id = Some(process_id.into());
240 self.cancellation_token = Some(cancellation_token);
241 self
242 }
243
244 pub(crate) fn process_events(
245 mut self,
246 process_id: impl Into<String>,
247 registry: Arc<dyn crate::ProcessRegistry>,
248 awaiter: crate::ProcessAwaiter,
249 store: Option<Arc<dyn crate::RuntimePersistence>>,
250 session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
251 queued_work_driver: Option<crate::QueuedWorkDriver>,
252 ) -> Self {
253 self.process_events = Some(ToolProcessEventContext {
254 process_id: process_id.into(),
255 registry,
256 awaiter,
257 store,
258 session_store_factory,
259 session_graph: Arc::clone(&self.session_graph),
260 queued_work_driver,
261 });
262 self
263 }
264
265 pub(crate) fn parent_invocation(mut self, metadata: Option<crate::RuntimeInvocation>) -> Self {
266 self.parent_invocation = metadata;
267 self
268 }
269
270 pub(crate) fn child_execution_trace_hook(
271 mut self,
272 hook: Option<ToolChildExecutionTraceHook>,
273 ) -> Self {
274 self.child_execution_trace_hook = hook;
275 self
276 }
277
278 pub(crate) fn build(self) -> ToolContext<'run> {
279 ToolContext {
280 session_id: self.session_id,
281 agent_frame_id: self.agent_frame_id,
282 sessions: self.sessions,
283 session_lifecycle: self.session_lifecycle,
284 processes: self.processes,
285 process_cancel_ability: self.process_cancel_ability,
286 effect_controller: self.effect_controller,
287 runtime_dispatch: self.runtime_dispatch,
288 runtime_execution_context: self.runtime_execution_context,
289 cancellation_token: self.cancellation_token,
290 async_process_id: self.async_process_id,
291 runtime_process_id: self.runtime_process_id,
292 process_events: self.process_events,
293 attachment_store: self.attachment_store,
294 direct_completions: self.direct_completions,
295 prepared_payload: self.prepared_payload,
296 tool_execution_binding: self.tool_execution_binding,
297 tool_call_id: self.tool_call_id,
298 attempt_number: 1,
299 max_attempts: 1,
300 replay_key: None,
301 completion: self.completion,
302 parent_invocation: self.parent_invocation,
303 execution_env_spec: self.execution_env_spec,
304 child_execution_trace_hook: self.child_execution_trace_hook,
305 }
306 }
307}
308
309impl<'run> ToolContext<'run> {
310 #[cfg(any(test, feature = "testing"))]
311 #[expect(
312 clippy::too_many_arguments,
313 reason = "testing constructor mirrors the sealed runtime tool context dependencies"
314 )]
315 pub(crate) fn builder(
316 session_id: String,
317 sessions: Arc<dyn SessionStateService>,
318 session_lifecycle: Arc<dyn SessionLifecycleService>,
319 session_graph: Arc<dyn SessionGraphService>,
320 processes: Arc<dyn crate::ProcessService>,
321 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
322 effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
323 attachment_store: Arc<crate::SessionAttachmentStore>,
324 direct_completions: crate::DirectCompletionClient<'run>,
325 ) -> ToolContextBuilder<'run> {
326 ToolContextBuilder {
327 session_id,
328 agent_frame_id: String::new(),
329 sessions,
330 session_lifecycle,
331 session_graph,
332 processes,
333 process_cancel_ability,
334 effect_controller,
335 runtime_dispatch: None,
336 runtime_execution_context: None,
337 cancellation_token: None,
338 async_process_id: None,
339 runtime_process_id: None,
340 process_events: None,
341 attachment_store,
342 direct_completions,
343 prepared_payload: serde_json::Value::Null,
344 tool_execution_binding: serde_json::Value::Null,
345 tool_call_id: None,
346 completion: ToolCompletionState::default(),
347 parent_invocation: None,
348 execution_env_spec: crate::ProcessExecutionEnvSpec::new(
349 crate::PluginOptions::default(),
350 crate::SessionPolicy::default(),
351 ),
352 child_execution_trace_hook: None,
353 }
354 }
355
356 pub(crate) fn from_dispatch(
357 dispatch: Arc<crate::tool_dispatch::ToolDispatchContext<'run>>,
358 ) -> ToolContextBuilder<'run> {
359 ToolContextBuilder::from_dispatch(dispatch)
360 }
361
362 pub fn session_id(&self) -> &str {
363 &self.session_id
364 }
365
366 pub fn agent_frame_id(&self) -> &str {
367 &self.agent_frame_id
368 }
369
370 pub fn sessions(&self) -> ToolSessionAdmin<'run> {
371 ToolSessionAdmin {
372 session_id: self.session_id.clone(),
373 sessions: Arc::clone(&self.sessions),
374 session_lifecycle: Arc::clone(&self.session_lifecycle),
375 effect_controller: self.effect_controller.clone(),
376 }
377 }
378
379 pub fn dispatch(&self) -> ToolDispatchClient<'run> {
380 ToolDispatchClient {
381 context: self.clone(),
382 }
383 }
384
385 pub fn triggers(&self) -> ToolTriggerClient<'run> {
386 ToolTriggerClient {
387 context: self.clone(),
388 }
389 }
390
391 pub fn processes(&self) -> ToolSessionProcessAdmin<'run> {
392 ToolSessionProcessAdmin {
393 session_id: self.session_id.clone(),
394 agent_frame_id: self.agent_frame_id.clone(),
395 processes: Arc::clone(&self.processes),
396 process_cancel_ability: Arc::clone(&self.process_cancel_ability),
397 effect_controller: self.effect_controller.clone(),
398 parent_invocation: self.parent_invocation.clone(),
399 tool_call_id: self.tool_call_id.clone(),
400 execution_env_spec: self.execution_env_spec.clone(),
401 }
402 }
403
404 pub fn emit_child_process_started(
405 &self,
406 process_id: impl Into<String>,
407 child_entry_name: Option<String>,
408 ) {
409 let Some(hook) = &self.child_execution_trace_hook else {
410 return;
411 };
412 hook.child_process_started(ToolChildProcessStarted {
413 process_id: process_id.into(),
414 child_entry_name,
415 });
416 }
417
418 pub fn direct_completions(&self) -> ToolDirectCompletionClient<'run> {
419 ToolDirectCompletionClient {
420 session_id: self.session_id.clone(),
421 tool_call_id: self.tool_call_id.clone(),
422 direct_completions: self.direct_completions.clone(),
423 parent_invocation: self.parent_invocation.clone(),
424 }
425 }
426
427 pub fn attachments(&self) -> ToolAttachmentClient {
428 ToolAttachmentClient {
429 store: Arc::clone(&self.attachment_store),
430 }
431 }
432
433 pub fn process_events(&self) -> ToolProcessEventClient {
434 ToolProcessEventClient {
435 context: self.process_events.clone(),
436 }
437 }
438
439 pub fn cancellation_token(&self) -> Option<&tokio_util::sync::CancellationToken> {
440 self.cancellation_token.as_ref()
441 }
442
443 #[doc(hidden)]
444 pub fn named_phase(&self, phase: &'static str) -> crate::runtime::RuntimeNamedPhase {
445 match self.runtime_execution_context.as_ref() {
446 Some(context) => context.named_phase(phase),
447 None => crate::runtime::RuntimeNamedPhase::begin(None, phase),
448 }
449 }
450
451 pub fn async_process_id(&self) -> Option<&str> {
452 self.async_process_id.as_deref()
453 }
454
455 pub fn runtime_process_id(&self) -> Option<&str> {
456 self.async_process_id
457 .as_deref()
458 .or(self.runtime_process_id.as_deref())
459 .or_else(|| {
460 self.process_events
461 .as_ref()
462 .map(|context| context.process_id.as_str())
463 })
464 }
465
466 pub fn tool_call_id(&self) -> Option<&str> {
467 self.tool_call_id.as_deref()
468 }
469
470 pub fn prepared_payload(&self) -> &serde_json::Value {
471 &self.prepared_payload
472 }
473
474 pub fn tool_execution_binding(&self) -> &serde_json::Value {
475 &self.tool_execution_binding
476 }
477
478 pub fn decode_prepared_payload<T>(&self) -> Result<T, serde_json::Error>
479 where
480 T: serde::de::DeserializeOwned,
481 {
482 serde_json::from_value(self.prepared_payload.clone())
483 }
484
485 pub fn attempt_number(&self) -> u32 {
486 self.attempt_number
487 }
488
489 pub fn max_attempts(&self) -> u32 {
490 self.max_attempts
491 }
492
493 pub fn replay_key(&self) -> Option<&str> {
494 self.replay_key.as_deref()
495 }
496
497 pub async fn completion_key(&self) -> Result<crate::AwaitEventKey, crate::RuntimeError> {
511 let tool_call_id = self.tool_call_id.clone().ok_or_else(|| {
512 crate::RuntimeError::new(
513 "tool_completion_key_missing_call_id",
514 "completion keys require a prepared tool call id",
515 )
516 })?;
517 let scoped = self.effect_controller.scoped();
518 let key = scoped
519 .controller()
520 .await_event_key(
521 scoped.execution_scope(),
522 crate::AwaitEventWaitIdentity::tool_completion(tool_call_id),
523 )
524 .await?;
525 self.completion.store(key)
526 }
527
528 pub(crate) fn take_completion_key(
529 &self,
530 ) -> Result<Option<crate::AwaitEventKey>, crate::RuntimeError> {
531 self.completion.take()
532 }
533
534 pub fn with_async_process(
535 mut self,
536 process_id: impl Into<String>,
537 cancellation_token: tokio_util::sync::CancellationToken,
538 ) -> Self {
539 self.async_process_id = Some(process_id.into());
540 self.runtime_process_id = self.async_process_id.clone();
541 self.cancellation_token = Some(cancellation_token);
542 self
543 }
544
545 #[cfg(any(test, feature = "testing"))]
546 #[doc(hidden)]
547 pub fn with_process_events_for_testing(
548 mut self,
549 process_id: impl Into<String>,
550 registry: Arc<dyn crate::ProcessRegistry>,
551 ) -> Self {
552 let awaiter = crate::ProcessAwaiter::polling(Arc::clone(®istry));
553 self.process_events = Some(ToolProcessEventContext {
554 process_id: process_id.into(),
555 registry,
556 awaiter,
557 store: None,
558 session_store_factory: None,
559 session_graph: Arc::new(crate::plugin::NoopSessionManager),
560 queued_work_driver: None,
561 });
562 self
563 }
564
565 pub(crate) fn with_retry_context(
566 mut self,
567 tool_name: &str,
568 attempt_number: u32,
569 max_attempts: u32,
570 ) -> Self {
571 self.attempt_number = attempt_number.max(1);
572 self.max_attempts = max_attempts.max(1);
573 self.replay_key = self
574 .tool_call_id
575 .as_ref()
576 .map(|call_id| format!("lash-tool:{}:{call_id}:{tool_name}", self.session_id));
577 self
578 }
579
580 pub(crate) fn with_prepared_payload(mut self, payload: serde_json::Value) -> Self {
581 self.prepared_payload = payload;
582 self
583 }
584
585 pub(crate) fn with_tool_execution_binding(mut self, binding: serde_json::Value) -> Self {
586 self.tool_execution_binding = binding;
587 self
588 }
589
590 #[cfg(any(test, feature = "testing"))]
593 #[doc(hidden)]
594 #[expect(
595 clippy::too_many_arguments,
596 reason = "test-only constructor mirrors the sealed runtime tool context"
597 )]
598 pub fn __for_testing(
599 session_id: String,
600 sessions: Arc<dyn SessionStateService>,
601 session_lifecycle: Arc<dyn SessionLifecycleService>,
602 session_graph: Arc<dyn SessionGraphService>,
603 processes: Arc<dyn crate::ProcessService>,
604 attachment_store: Arc<crate::SessionAttachmentStore>,
605 direct_completions: crate::DirectCompletionClient<'static>,
606 tool_call_id: Option<String>,
607 ) -> ToolContext<'static> {
608 ToolContext::builder(
609 session_id,
610 sessions,
611 session_lifecycle,
612 session_graph,
613 processes,
614 Arc::new(crate::DefaultProcessCancelAbility),
615 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
616 crate::InlineRuntimeEffectController,
617 )),
618 attachment_store,
619 direct_completions,
620 )
621 .tool_call_id(tool_call_id)
622 .build()
623 }
624
625 #[cfg(any(test, feature = "testing"))]
629 #[doc(hidden)]
630 #[expect(
631 clippy::too_many_arguments,
632 reason = "test-only constructor mirrors the sealed runtime context"
633 )]
634 pub fn __for_testing_with_process_cancel_ability(
635 session_id: String,
636 sessions: Arc<dyn SessionStateService>,
637 session_lifecycle: Arc<dyn SessionLifecycleService>,
638 session_graph: Arc<dyn SessionGraphService>,
639 processes: Arc<dyn crate::ProcessService>,
640 process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
641 attachment_store: Arc<crate::SessionAttachmentStore>,
642 direct_completions: crate::DirectCompletionClient<'static>,
643 tool_call_id: Option<String>,
644 ) -> ToolContext<'static> {
645 ToolContext::builder(
646 session_id,
647 sessions,
648 session_lifecycle,
649 session_graph,
650 processes,
651 process_cancel_ability,
652 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
653 crate::InlineRuntimeEffectController,
654 )),
655 attachment_store,
656 direct_completions,
657 )
658 .tool_call_id(tool_call_id)
659 .build()
660 }
661}
662
663#[derive(Clone, Debug, Serialize, Deserialize)]
669pub struct PreparedToolCall {
670 pub call_id: String,
671 pub tool_id: ToolId,
672 pub tool_name: String,
673 pub args: serde_json::Value,
674 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub replay: Option<ProviderReplayMeta>,
676 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
677 pub prepared_payload: serde_json::Value,
678}
679
680impl PreparedToolCall {
681 pub fn identity(tool_id: ToolId, call: crate::sansio::PendingToolCall) -> Self {
682 Self {
683 call_id: call.call_id,
684 tool_id,
685 tool_name: call.tool_name,
686 args: call.args,
687 replay: call.replay,
688 prepared_payload: serde_json::Value::Null,
689 }
690 }
691
692 pub fn from_parts(
693 call_id: impl Into<String>,
694 tool_id: impl Into<ToolId>,
695 tool_name: impl Into<String>,
696 args: serde_json::Value,
697 replay: Option<ProviderReplayMeta>,
698 prepared_payload: serde_json::Value,
699 ) -> Self {
700 Self {
701 call_id: call_id.into(),
702 tool_id: tool_id.into(),
703 tool_name: tool_name.into(),
704 args,
705 replay,
706 prepared_payload,
707 }
708 }
709}
710
711#[derive(Clone, Debug, Serialize, Deserialize)]
717pub struct PreparedToolBatchCall {
718 pub call: PreparedToolCall,
719 pub replay_suffix: String,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub execution_grant: Option<Box<ToolExecutionGrant>>,
722}
723
724#[derive(Clone, Debug, Serialize, Deserialize)]
730pub struct PreparedToolBatch {
731 pub batch_id: String,
732 pub calls: Vec<PreparedToolBatchCall>,
733}
734
735impl PreparedToolBatch {
736 pub fn new(batch_id: impl Into<String>, calls: Vec<PreparedToolCall>) -> Self {
737 let batch_id = batch_id.into();
738 let calls = calls
739 .into_iter()
740 .enumerate()
741 .map(|(index, call)| PreparedToolBatchCall {
742 replay_suffix: format!("child:{index}:{}", call.call_id),
743 call,
744 execution_grant: None,
745 })
746 .collect();
747 Self { batch_id, calls }
748 }
749
750 pub fn new_with_grants(
751 batch_id: impl Into<String>,
752 calls: Vec<(PreparedToolCall, Option<ToolExecutionGrant>)>,
753 ) -> Self {
754 let batch_id = batch_id.into();
755 let calls = calls
756 .into_iter()
757 .enumerate()
758 .map(|(index, (call, execution_grant))| PreparedToolBatchCall {
759 replay_suffix: format!("child:{index}:{}", call.call_id),
760 call,
761 execution_grant: execution_grant.map(Box::new),
762 })
763 .collect();
764 Self { batch_id, calls }
765 }
766
767 pub fn is_empty(&self) -> bool {
768 self.calls.is_empty()
769 }
770
771 pub fn len(&self) -> usize {
772 self.calls.len()
773 }
774}
775
776#[derive(Clone, Debug, Serialize, Deserialize)]
784pub struct ToolExecutionGrant {
785 pub manifest: ToolManifest,
787 pub contract: Box<ToolContract>,
790 pub source_id: Option<String>,
793 pub execution_binding: serde_json::Value,
795}
796
797impl ToolExecutionGrant {
798 pub fn new(manifest: ToolManifest, contract: ToolContract) -> Self {
799 Self {
800 manifest,
801 contract: Box::new(contract),
802 source_id: None,
803 execution_binding: serde_json::Value::Null,
804 }
805 }
806
807 pub fn from_definition(definition: ToolDefinition) -> Self {
808 Self::new(definition.manifest(), definition.contract())
809 }
810
811 pub fn with_source_id(mut self, source_id: impl Into<String>) -> Self {
812 self.source_id = Some(source_id.into());
813 self
814 }
815
816 pub fn with_execution_binding(mut self, execution_binding: serde_json::Value) -> Self {
817 self.execution_binding = execution_binding;
818 self
819 }
820}
821
822#[derive(Clone)]
823pub struct ToolPrepareContext {
824 session_id: String,
825 sessions: Arc<dyn SessionStateService>,
826 turn_context: crate::TurnContext,
827 tool_call_id: Option<String>,
828 tool_execution_binding: serde_json::Value,
829}
830
831impl ToolPrepareContext {
832 pub(crate) fn with_execution_binding(
833 session_id: String,
834 sessions: Arc<dyn SessionStateService>,
835 turn_context: crate::TurnContext,
836 tool_call_id: Option<String>,
837 tool_execution_binding: serde_json::Value,
838 ) -> Self {
839 Self {
840 session_id,
841 sessions,
842 turn_context,
843 tool_call_id,
844 tool_execution_binding,
845 }
846 }
847
848 pub fn session_id(&self) -> &str {
849 &self.session_id
850 }
851
852 pub fn tool_call_id(&self) -> Option<&str> {
853 self.tool_call_id.as_deref()
854 }
855
856 pub fn tool_execution_binding(&self) -> &serde_json::Value {
857 &self.tool_execution_binding
858 }
859
860 pub fn turn_context(&self) -> &crate::TurnContext {
861 &self.turn_context
862 }
863
864 pub fn plugin_input<T>(&self, plugin_id: &'static str) -> Option<&T>
865 where
866 T: 'static,
867 {
868 self.turn_context.plugin_input::<T>(plugin_id)
869 }
870
871 pub async fn session_snapshot(&self) -> Result<SessionSnapshot, PluginError> {
872 self.sessions.snapshot_session(&self.session_id).await
873 }
874
875 pub async fn tool_catalog(&self) -> Result<Vec<serde_json::Value>, PluginError> {
876 self.sessions.tool_catalog(&self.session_id).await
877 }
878
879 pub async fn shared_tool_catalog(
880 &self,
881 ) -> Result<std::sync::Arc<Vec<serde_json::Value>>, PluginError> {
882 self.sessions.shared_tool_catalog(&self.session_id).await
883 }
884}
885
886pub struct ToolPrepareCall<'a> {
888 pub tool_id: ToolId,
889 pub pending: crate::sansio::PendingToolCall,
890 pub context: &'a ToolPrepareContext,
891}
892
893pub struct ToolCall<'a> {
900 pub name: &'a str,
901 pub args: &'a serde_json::Value,
902 pub context: &'a ToolContext<'a>,
903 pub progress: Option<&'a ProgressSender>,
904}
905
906#[async_trait::async_trait]
914pub trait ToolProvider: Send + Sync + 'static {
915 fn tool_manifests(&self) -> Vec<ToolManifest>;
916 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
917 self.tool_manifests()
918 .into_iter()
919 .find(|manifest| manifest.name == name)
920 }
921 fn resolve_manifest_by_id(&self, id: &ToolId) -> Option<ToolManifest> {
922 self.tool_manifests()
923 .into_iter()
924 .find(|manifest| manifest.id == *id)
925 }
926 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>>;
927 fn resolve_contract_by_id(&self, id: &ToolId) -> Option<Arc<ToolContract>> {
928 let manifest = self.resolve_manifest_by_id(id)?;
929 self.resolve_contract(&manifest.name)
930 }
931 async fn prepare_tool_call(
932 &self,
933 call: ToolPrepareCall<'_>,
934 ) -> Result<PreparedToolCall, ToolResult> {
935 Ok(PreparedToolCall::identity(call.tool_id, call.pending))
936 }
937 async fn prepare_granted_tool_call(
938 &self,
939 grant: &ToolExecutionGrant,
940 call: ToolPrepareCall<'_>,
941 ) -> Result<PreparedToolCall, ToolResult> {
942 let _ = call;
943 Err(ToolResult::err_fmt(format_args!(
944 "Granted execution is unsupported for tool id `{}`",
945 grant.manifest.id
946 )))
947 }
948 async fn execute(&self, call: ToolCall<'_>) -> ToolResult;
949 async fn execute_granted(
950 &self,
951 grant: &ToolExecutionGrant,
952 args: &serde_json::Value,
953 context: &ToolContext<'_>,
954 progress: Option<&ProgressSender>,
955 ) -> ToolResult {
956 let _ = (args, context, progress);
957 ToolResult::err_fmt(format_args!(
958 "Granted execution is unsupported for tool id `{}`",
959 grant.manifest.id
960 ))
961 }
962 async fn execute_by_id(
963 &self,
964 tool_id: &ToolId,
965 args: &serde_json::Value,
966 context: &ToolContext<'_>,
967 progress: Option<&ProgressSender>,
968 ) -> ToolResult {
969 let Some(manifest) = self.resolve_manifest_by_id(tool_id) else {
970 return ToolResult::err_fmt(format!("Unknown tool id: {tool_id}"));
971 };
972 self.execute(ToolCall {
973 name: &manifest.name,
974 args,
975 context,
976 progress,
977 })
978 .await
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[test]
987 fn tool_context_builder_carries_call_payload_and_cancellation_state() {
988 let cancellation = tokio_util::sync::CancellationToken::new();
989 let prepared = PreparedToolCall::from_parts(
990 "call-1",
991 "tool:demo_tool",
992 "demo_tool",
993 serde_json::json!({ "input": true }),
994 None,
995 serde_json::json!({ "prepared": true }),
996 );
997
998 let context = ToolContext::builder(
999 "session-1".to_string(),
1000 Arc::new(crate::testing::MockSessionManager::default()),
1001 Arc::new(crate::testing::MockSessionManager::default()),
1002 Arc::new(crate::testing::MockSessionManager::default()),
1003 Arc::new(crate::UnavailableProcessService),
1004 Arc::new(crate::DefaultProcessCancelAbility),
1005 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
1006 crate::InlineRuntimeEffectController,
1007 )),
1008 Arc::new(crate::SessionAttachmentStore::in_memory()),
1009 crate::DirectCompletionClient::unavailable(
1010 "direct completions are unavailable in this test context",
1011 ),
1012 )
1013 .prepared_call(&prepared)
1014 .cancellation_token(Some(cancellation.clone()))
1015 .async_process("process-1", cancellation.clone())
1016 .build();
1017
1018 assert_eq!(context.session_id(), "session-1");
1019 assert_eq!(context.tool_call_id(), Some("call-1"));
1020 assert_eq!(
1021 context.prepared_payload(),
1022 &serde_json::json!({ "prepared": true })
1023 );
1024 assert_eq!(context.async_process_id(), Some("process-1"));
1025 assert!(context.cancellation_token().is_some());
1026 }
1027}