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