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