Skip to main content

lash_core/
tool_provider.rs

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