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