1use std::borrow::Cow;
58use std::fmt::Write as _;
59use std::hash::Hasher;
60use std::marker::PhantomData;
61use std::path::PathBuf;
62use std::sync::Arc;
63use std::time::{Duration, Instant};
64
65use anyhow::{Error, Result};
66use async_trait::async_trait;
67use serde_json::Value;
68use vtcode_commons::serde_helpers::json_to_string_pretty;
69
70use crate::cache::{CacheKey, UnifiedCache, estimate_json_size};
71use crate::tool_policy::ToolPolicy;
72use crate::tools::handlers::tool_handler::{
73 ToolCallError, ToolHandler, ToolInvocation, ToolKind, ToolOutput, ToolPayload,
74};
75use crate::tools::result::ToolResult as SplitToolResult;
76use crate::tools::traits::Tool;
77
78const MAX_RETRY_ATTEMPTS: u32 = 16;
79const MAX_RETRY_BACKOFF: Duration = Duration::from_secs(30);
80
81pub trait HasComponent<Name> {
91 type Provider;
93}
94
95pub type ComponentProvider<Ctx, Name> = <Ctx as HasComponent<Name>>::Provider;
97
98#[macro_export]
109macro_rules! delegate_components {
110 ($ctx:ty { $($name:ty => $provider:ty),* $(,)? }) => {
111 $(
112 impl $crate::components::HasComponent<$name> for $ctx {
113 type Provider = $provider;
114 }
115 )*
116 };
117}
118
119pub enum ApprovalComponent {}
125
126pub enum SandboxComponent {}
128
129pub enum ExecuteComponent {}
131
132pub enum MetadataComponent {}
134
135pub enum SessionComponent {}
137
138pub enum OutputMapComponent {}
140
141pub enum LoggingComponent {}
143
144pub enum CacheComponent {}
146
147pub enum RetryComponent {}
149
150#[async_trait]
160pub trait ApprovalProvider<Ctx: Send + Sync>: Send + Sync {
161 async fn check_approval(ctx: &Ctx, tool_name: &str, description: &str) -> Result<()>;
164}
165
166pub trait SandboxProvider<Ctx: Send + Sync>: Send + Sync {
168 fn sandbox_enabled(ctx: &Ctx) -> bool;
170
171 fn workspace_root(ctx: &Ctx) -> Option<&PathBuf>;
173}
174
175pub trait MetadataProvider<Ctx>: Send + Sync {
177 fn tool_name(_ctx: &Ctx) -> &str {
178 "unknown"
179 }
180
181 fn tool_description(_ctx: &Ctx) -> &str {
182 ""
183 }
184
185 fn parameter_schema(_ctx: &Ctx) -> Option<Value> {
186 None
187 }
188
189 fn config_schema(_ctx: &Ctx) -> Option<Value> {
190 None
191 }
192
193 fn state_schema(_ctx: &Ctx) -> Option<Value> {
194 None
195 }
196
197 fn prompt_path(_ctx: &Ctx) -> Option<Cow<'static, str>> {
198 None
199 }
200
201 fn default_permission(_ctx: &Ctx) -> ToolPolicy {
202 ToolPolicy::Prompt
203 }
204
205 fn allow_patterns(_ctx: &Ctx) -> Option<&'static [&'static str]> {
206 None
207 }
208
209 fn deny_patterns(_ctx: &Ctx) -> Option<&'static [&'static str]> {
210 None
211 }
212
213 fn is_mutating(_ctx: &Ctx) -> bool {
214 true
215 }
216
217 fn is_parallel_safe(ctx: &Ctx) -> bool {
218 !Self::is_mutating(ctx)
219 }
220
221 fn tool_kind(_ctx: &Ctx) -> &'static str {
222 "unknown"
223 }
224
225 fn resource_hints(_ctx: &Ctx, _args: &Value) -> Vec<String> {
226 Vec::new()
227 }
228
229 fn execution_cost(_ctx: &Ctx) -> u8 {
230 5
231 }
232}
233
234pub trait OutputMapProvider<Ctx>: Send + Sync {
236 type Input;
237 type Output;
238
239 fn map_output(ctx: &Ctx, input: Self::Input) -> Self::Output;
240}
241
242#[async_trait]
248pub trait ExecuteProvider<Ctx: Send + Sync>: Send + Sync {
249 async fn execute(ctx: &Ctx, args: Value) -> Result<Value>;
251
252 async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
256 let result = Self::execute(ctx, args).await?;
257 let name = if let Some(n) = result.get("tool_name").and_then(|v| v.as_str()) {
258 n.to_string()
259 } else {
260 "unknown".to_string()
261 };
262 let content = value_to_text(&result);
263 Ok(SplitToolResult::simple(&name, content))
264 }
265}
266
267pub trait LoggingProvider<Ctx>: Send + Sync {
269 fn on_start(_ctx: &Ctx, _tool_name: &str, _args: &Value) {}
270
271 fn on_cache_hit(_ctx: &Ctx, _tool_name: &str, _args: &Value) {}
272
273 fn on_success(
274 _ctx: &Ctx,
275 _tool_name: &str,
276 _duration: Duration,
277 _attempt: u32,
278 _from_cache: bool,
279 ) {
280 }
281
282 fn on_retry(
283 _ctx: &Ctx,
284 _tool_name: &str,
285 _next_attempt: u32,
286 _backoff: Duration,
287 _error: &Error,
288 ) {
289 }
290
291 fn on_failure(
292 _ctx: &Ctx,
293 _tool_name: &str,
294 _duration: Duration,
295 _attempt: u32,
296 _error: &Error,
297 ) {
298 }
299}
300
301pub trait CacheProvider<Ctx>: Send + Sync {
303 fn get_json(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> Option<Value> {
304 None
305 }
306
307 fn put_json(_ctx: &Ctx, _tool_name: &str, _args: &Value, _result: &Value) {}
308
309 fn get_dual(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> Option<SplitToolResult> {
310 None
311 }
312
313 fn put_dual(_ctx: &Ctx, _tool_name: &str, _args: &Value, _result: &SplitToolResult) {}
314}
315
316pub trait RetryProvider<Ctx>: Send + Sync {
318 fn max_attempts(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> u32 {
319 1
320 }
321
322 fn should_retry(_ctx: &Ctx, _tool_name: &str, _attempt: u32, _error: &Error) -> bool {
323 false
324 }
325
326 fn backoff_duration(_ctx: &Ctx, _tool_name: &str, _attempt: u32) -> Duration {
327 Duration::ZERO
328 }
329}
330
331pub struct AutoApproval;
337
338#[async_trait]
339impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for AutoApproval {
340 async fn check_approval(_ctx: &Ctx, _tool_name: &str, _description: &str) -> Result<()> {
341 Ok(())
342 }
343}
344
345pub struct DenyAllApproval;
347
348#[async_trait]
349impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for DenyAllApproval {
350 async fn check_approval(_ctx: &Ctx, tool_name: &str, _description: &str) -> Result<()> {
351 anyhow::bail!("operation denied: {tool_name} is not permitted in this context")
352 }
353}
354
355pub struct NoSandbox;
357
358#[async_trait]
359impl<Ctx: Send + Sync> SandboxProvider<Ctx> for NoSandbox {
360 fn sandbox_enabled(_ctx: &Ctx) -> bool {
361 false
362 }
363
364 fn workspace_root(_ctx: &Ctx) -> Option<&PathBuf> {
365 None
366 }
367}
368
369pub struct DefaultMetadata;
371
372impl<Ctx> MetadataProvider<Ctx> for DefaultMetadata {}
373
374pub struct NoLogging;
376
377impl<Ctx> LoggingProvider<Ctx> for NoLogging {}
378
379pub struct NoCache;
381
382impl<Ctx> CacheProvider<Ctx> for NoCache {}
383
384pub struct NoRetry;
386
387impl<Ctx> RetryProvider<Ctx> for NoRetry {}
388
389pub struct TracingLogging;
391
392impl<Ctx> LoggingProvider<Ctx> for TracingLogging {
393 fn on_start(_ctx: &Ctx, tool_name: &str, _args: &Value) {
394 tracing::trace!(tool = %tool_name, "CGP tool execution started");
395 }
396
397 fn on_cache_hit(_ctx: &Ctx, tool_name: &str, _args: &Value) {
398 tracing::debug!(tool = %tool_name, "CGP tool result served from cache");
399 }
400
401 fn on_success(_ctx: &Ctx, tool_name: &str, duration: Duration, attempt: u32, from_cache: bool) {
402 tracing::trace!(
403 tool = %tool_name,
404 duration_ms = duration.as_millis() as u64,
405 attempt,
406 from_cache,
407 "CGP tool execution succeeded"
408 );
409 }
410
411 fn on_retry(_ctx: &Ctx, tool_name: &str, next_attempt: u32, backoff: Duration, error: &Error) {
412 tracing::debug!(
413 tool = %tool_name,
414 next_attempt,
415 backoff_ms = backoff.as_millis() as u64,
416 error = %error,
417 "CGP tool execution retry scheduled"
418 );
419 }
420
421 fn on_failure(_ctx: &Ctx, tool_name: &str, duration: Duration, attempt: u32, error: &Error) {
422 tracing::warn!(
423 tool = %tool_name,
424 duration_ms = duration.as_millis() as u64,
425 attempt,
426 error = %error,
427 "CGP tool execution failed"
428 );
429 }
430}
431
432#[derive(Debug, Clone, Copy)]
434pub struct RetryPolicy {
435 pub max_attempts: u32,
436 pub initial_backoff: Duration,
437 pub max_backoff: Duration,
438}
439
440impl Default for RetryPolicy {
441 fn default() -> Self {
442 Self {
443 max_attempts: 3,
444 initial_backoff: Duration::from_millis(100),
445 max_backoff: Duration::from_secs(1),
446 }
447 }
448}
449
450pub trait HasRetryPolicy: Send + Sync {
452 fn retry_policy(&self) -> RetryPolicy;
453}
454
455pub struct ExponentialBackoffRetry;
460
461impl<Ctx: HasRetryPolicy> RetryProvider<Ctx> for ExponentialBackoffRetry {
462 fn max_attempts(ctx: &Ctx, _tool_name: &str, _args: &Value) -> u32 {
463 ctx.retry_policy().max_attempts.max(1)
464 }
465
466 fn should_retry(_ctx: &Ctx, _tool_name: &str, _attempt: u32, _error: &Error) -> bool {
467 true
468 }
469
470 fn backoff_duration(ctx: &Ctx, _tool_name: &str, attempt: u32) -> Duration {
471 let policy = ctx.retry_policy();
472 let exponent = attempt.saturating_sub(1).min(31);
473 let factor = 2_u64.saturating_pow(exponent);
474 let millis = policy.initial_backoff.as_millis() as u64;
475 Duration::from_millis(millis.saturating_mul(factor)).min(policy.max_backoff)
476 }
477}
478
479#[derive(Debug, Clone, Hash, PartialEq, Eq)]
481pub struct ToolExecutionCacheKey(String);
482
483impl CacheKey for ToolExecutionCacheKey {
484 fn to_cache_key(&self) -> String {
485 self.0.clone()
486 }
487}
488
489pub trait HasExecutionCaches: Send + Sync {
491 fn json_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, Value>;
492 fn dual_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, SplitToolResult>;
493}
494
495fn hash_json_value<H: Hasher>(hasher: &mut H, value: &Value) {
499 match value {
500 Value::Null => hasher.write_u8(0x00),
501 Value::Bool(b) => {
502 hasher.write_u8(0x01);
503 hasher.write_u8(u8::from(*b));
504 }
505 Value::Number(n) => {
506 hasher.write_u8(0x02);
507 let s = n.to_string();
508 hasher.write(s.as_bytes());
509 }
510 Value::String(s) => {
511 hasher.write_u8(0x03);
512 hasher.write(s.as_bytes());
513 }
514 Value::Array(arr) => {
515 hasher.write_u8(0x04);
516 hasher.write_usize(arr.len());
517 for item in arr {
518 hash_json_value(hasher, item);
519 }
520 }
521 Value::Object(map) => {
522 hasher.write_u8(0x05);
523 hasher.write_usize(map.len());
524 let mut keys: Vec<&String> = map.keys().collect();
526 keys.sort();
527 for k in keys {
528 hasher.write(k.as_bytes());
529 hash_json_value(hasher, map.get(k).unwrap());
530 }
531 }
532 }
533}
534
535fn build_execution_cache_key(tool_name: &str, args: &Value) -> ToolExecutionCacheKey {
536 let mut hasher = std::collections::hash_map::DefaultHasher::new();
537 hasher.write(tool_name.as_bytes());
538 hash_json_value(&mut hasher, args);
539
540 let mut cache_key = String::with_capacity(tool_name.len() + 22);
541 cache_key.push_str(tool_name);
542 cache_key.push_str("::");
543 let _ = write!(&mut cache_key, "{}", hasher.finish());
544
545 ToolExecutionCacheKey(cache_key)
546}
547
548pub struct CachedResults;
550
551impl<Ctx: HasExecutionCaches> CacheProvider<Ctx> for CachedResults {
552 fn get_json(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Value> {
553 ctx.json_cache()
554 .get_owned(&build_execution_cache_key(tool_name, args))
555 }
556
557 fn put_json(ctx: &Ctx, tool_name: &str, args: &Value, result: &Value) {
558 let key = build_execution_cache_key(tool_name, args);
559 let size = estimate_json_size(result);
560 ctx.json_cache().insert(key, result.clone(), size);
561 }
562
563 fn get_dual(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<SplitToolResult> {
564 ctx.dual_cache()
565 .get_owned(&build_execution_cache_key(tool_name, args))
566 }
567
568 fn put_dual(ctx: &Ctx, tool_name: &str, args: &Value, result: &SplitToolResult) {
569 let key = build_execution_cache_key(tool_name, args);
570 let size = (result.llm_content.len() + result.ui_content.len()) as u64;
571 ctx.dual_cache().insert(key, result.clone(), size);
572 }
573}
574
575pub struct PassthroughMetadata;
577
578impl<Ctx: HasToolRef> MetadataProvider<Ctx> for PassthroughMetadata {
579 fn tool_name(ctx: &Ctx) -> &str {
580 ctx.tool().name()
581 }
582
583 fn tool_description(ctx: &Ctx) -> &str {
584 ctx.tool().description()
585 }
586
587 fn parameter_schema(ctx: &Ctx) -> Option<Value> {
588 ctx.tool().parameter_schema()
589 }
590
591 fn config_schema(ctx: &Ctx) -> Option<Value> {
592 ctx.tool().config_schema()
593 }
594
595 fn state_schema(ctx: &Ctx) -> Option<Value> {
596 ctx.tool().state_schema()
597 }
598
599 fn prompt_path(ctx: &Ctx) -> Option<Cow<'static, str>> {
600 ctx.tool().prompt_path()
601 }
602
603 fn default_permission(ctx: &Ctx) -> ToolPolicy {
604 ctx.tool().default_permission()
605 }
606
607 fn allow_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
608 ctx.tool().allow_patterns()
609 }
610
611 fn deny_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
612 ctx.tool().deny_patterns()
613 }
614
615 fn is_mutating(ctx: &Ctx) -> bool {
616 ctx.tool().is_mutating()
617 }
618
619 fn is_parallel_safe(ctx: &Ctx) -> bool {
620 ctx.tool().is_parallel_safe()
621 }
622
623 fn tool_kind(ctx: &Ctx) -> &'static str {
624 ctx.tool().kind()
625 }
626
627 fn resource_hints(ctx: &Ctx, args: &Value) -> Vec<String> {
628 ctx.tool().resource_hints(args)
629 }
630
631 fn execution_cost(ctx: &Ctx) -> u8 {
632 ctx.tool().execution_cost()
633 }
634}
635
636fn value_to_text(value: &Value) -> String {
641 if value.is_string() {
642 value.as_str().unwrap_or("").to_string()
643 } else {
644 json_to_string_pretty(value)
645 }
646}
647
648#[async_trait]
649trait ExecutionMode<Ctx>
650where
651 Ctx: HasComponent<ExecuteComponent> + HasComponent<CacheComponent> + Send + Sync,
652 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
653 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
654{
655 type Output: Send;
656
657 fn get_cached(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Self::Output>;
658
659 fn put_cached(ctx: &Ctx, tool_name: &str, args: &Value, result: &Self::Output);
660
661 async fn execute(ctx: &Ctx, args: Value) -> Result<Self::Output>;
662}
663
664struct JsonExecution;
665
666#[async_trait]
667impl<Ctx> ExecutionMode<Ctx> for JsonExecution
668where
669 Ctx: HasComponent<ExecuteComponent> + HasComponent<CacheComponent> + Send + Sync,
670 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
671 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
672{
673 type Output = Value;
674
675 fn get_cached(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Self::Output> {
676 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::get_json(
677 ctx, tool_name, args,
678 )
679 }
680
681 fn put_cached(ctx: &Ctx, tool_name: &str, args: &Value, result: &Self::Output) {
682 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::put_json(
683 ctx, tool_name, args, result,
684 );
685 }
686
687 async fn execute(ctx: &Ctx, args: Value) -> Result<Self::Output> {
688 <ComponentProvider<Ctx, ExecuteComponent> as ExecuteProvider<Ctx>>::execute(ctx, args).await
689 }
690}
691
692struct DualExecution;
693
694#[async_trait]
695impl<Ctx> ExecutionMode<Ctx> for DualExecution
696where
697 Ctx: HasComponent<ExecuteComponent> + HasComponent<CacheComponent> + Send + Sync,
698 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
699 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
700{
701 type Output = SplitToolResult;
702
703 fn get_cached(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Self::Output> {
704 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::get_dual(
705 ctx, tool_name, args,
706 )
707 }
708
709 fn put_cached(ctx: &Ctx, tool_name: &str, args: &Value, result: &Self::Output) {
710 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::put_dual(
711 ctx, tool_name, args, result,
712 );
713 }
714
715 async fn execute(ctx: &Ctx, args: Value) -> Result<Self::Output> {
716 <ComponentProvider<Ctx, ExecuteComponent> as ExecuteProvider<Ctx>>::execute_dual(ctx, args)
717 .await
718 }
719}
720
721fn retry_backoff<Ctx>(ctx: &Ctx, tool_name: &str, attempt: u32) -> Duration
722where
723 Ctx: HasComponent<RetryComponent> + Send + Sync,
724 ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
725{
726 <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::backoff_duration(
727 ctx, tool_name, attempt,
728 )
729 .min(MAX_RETRY_BACKOFF)
730}
731
732async fn execute_tool_with_mode<Ctx, Mode>(
733 ctx: &Ctx,
734 tool_name: &str,
735 args: Value,
736) -> Result<Mode::Output>
737where
738 Ctx: HasComponent<ExecuteComponent>
739 + HasComponent<LoggingComponent>
740 + HasComponent<CacheComponent>
741 + HasComponent<RetryComponent>
742 + Send
743 + Sync,
744 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
745 ComponentProvider<Ctx, LoggingComponent>: LoggingProvider<Ctx>,
746 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
747 ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
748 Mode: ExecutionMode<Ctx>,
749{
750 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_start(
751 ctx, tool_name, &args,
752 );
753
754 if let Some(result) = Mode::get_cached(ctx, tool_name, &args) {
755 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_cache_hit(
756 ctx, tool_name, &args,
757 );
758 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
759 ctx,
760 tool_name,
761 Duration::ZERO,
762 1,
763 true,
764 );
765 return Ok(result);
766 }
767
768 let started = Instant::now();
769 let max_attempts =
770 <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::max_attempts(
771 ctx, tool_name, &args,
772 )
773 .clamp(1, MAX_RETRY_ATTEMPTS);
774
775 let mut attempt = 1;
776 loop {
777 match Mode::execute(ctx, args.clone()).await {
778 Ok(result) => {
779 Mode::put_cached(ctx, tool_name, &args, &result);
780 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
781 ctx,
782 tool_name,
783 started.elapsed(),
784 attempt,
785 false,
786 );
787 return Ok(result);
788 }
789 Err(error) => {
790 let should_retry = attempt < max_attempts
791 && <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::should_retry(
792 ctx, tool_name, attempt, &error,
793 );
794
795 if !should_retry {
796 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_failure(
797 ctx,
798 tool_name,
799 started.elapsed(),
800 attempt,
801 &error,
802 );
803 return Err(error);
804 }
805
806 let backoff = retry_backoff(ctx, tool_name, attempt);
807 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_retry(
808 ctx,
809 tool_name,
810 attempt + 1,
811 backoff,
812 &error,
813 );
814 if !backoff.is_zero() {
815 tokio::time::sleep(backoff).await;
816 }
817 attempt += 1;
818 }
819 }
820 }
821}
822
823#[async_trait]
824pub trait CanApproveTool: Send + Sync {
825 async fn approve_tool(&self, tool_name: &str, description: &str) -> Result<()>;
826}
827
828#[async_trait]
829impl<Ctx> CanApproveTool for Ctx
830where
831 Ctx: HasComponent<ApprovalComponent> + Send + Sync,
832 ComponentProvider<Ctx, ApprovalComponent>: ApprovalProvider<Ctx>,
833{
834 async fn approve_tool(&self, tool_name: &str, description: &str) -> Result<()> {
835 <ComponentProvider<Ctx, ApprovalComponent> as ApprovalProvider<Ctx>>::check_approval(
836 self,
837 tool_name,
838 description,
839 )
840 .await
841 }
842}
843
844pub trait CanResolveSandbox: Send + Sync {
845 fn sandbox_enabled(&self) -> bool;
846
847 fn workspace_root(&self) -> Option<&PathBuf>;
848}
849
850impl<Ctx> CanResolveSandbox for Ctx
851where
852 Ctx: HasComponent<SandboxComponent> + Send + Sync,
853 ComponentProvider<Ctx, SandboxComponent>: SandboxProvider<Ctx>,
854{
855 fn sandbox_enabled(&self) -> bool {
856 <ComponentProvider<Ctx, SandboxComponent> as SandboxProvider<Ctx>>::sandbox_enabled(self)
857 }
858
859 fn workspace_root(&self) -> Option<&PathBuf> {
860 <ComponentProvider<Ctx, SandboxComponent> as SandboxProvider<Ctx>>::workspace_root(self)
861 }
862}
863
864pub trait CanProvideToolMetadata: Send + Sync {
865 fn tool_name(&self) -> &str;
866
867 fn tool_description(&self) -> &str;
868
869 fn parameter_schema(&self) -> Option<Value>;
870
871 fn config_schema(&self) -> Option<Value>;
872
873 fn state_schema(&self) -> Option<Value>;
874
875 fn prompt_path(&self) -> Option<Cow<'static, str>>;
876
877 fn default_permission(&self) -> ToolPolicy;
878
879 fn allow_patterns(&self) -> Option<&'static [&'static str]>;
880
881 fn deny_patterns(&self) -> Option<&'static [&'static str]>;
882
883 fn is_mutating(&self) -> bool;
884
885 fn is_parallel_safe(&self) -> bool;
886
887 fn tool_kind(&self) -> &'static str;
888
889 fn resource_hints(&self, args: &Value) -> Vec<String>;
890
891 fn execution_cost(&self) -> u8;
892}
893
894impl<Ctx> CanProvideToolMetadata for Ctx
895where
896 Ctx: HasComponent<MetadataComponent> + Send + Sync,
897 ComponentProvider<Ctx, MetadataComponent>: MetadataProvider<Ctx>,
898{
899 fn tool_name(&self) -> &str {
900 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_name(self)
901 }
902
903 fn tool_description(&self) -> &str {
904 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_description(self)
905 }
906
907 fn parameter_schema(&self) -> Option<Value> {
908 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::parameter_schema(self)
909 }
910
911 fn config_schema(&self) -> Option<Value> {
912 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::config_schema(self)
913 }
914
915 fn state_schema(&self) -> Option<Value> {
916 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::state_schema(self)
917 }
918
919 fn prompt_path(&self) -> Option<Cow<'static, str>> {
920 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::prompt_path(self)
921 }
922
923 fn default_permission(&self) -> ToolPolicy {
924 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::default_permission(
925 self,
926 )
927 }
928
929 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
930 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::allow_patterns(self)
931 }
932
933 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
934 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::deny_patterns(self)
935 }
936
937 fn is_mutating(&self) -> bool {
938 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::is_mutating(self)
939 }
940
941 fn is_parallel_safe(&self) -> bool {
942 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::is_parallel_safe(self)
943 }
944
945 fn tool_kind(&self) -> &'static str {
946 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_kind(self)
947 }
948
949 fn resource_hints(&self, args: &Value) -> Vec<String> {
950 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::resource_hints(
951 self, args,
952 )
953 }
954
955 fn execution_cost(&self) -> u8 {
956 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::execution_cost(self)
957 }
958}
959
960#[async_trait]
961pub trait CanExecuteTool: Send + Sync {
962 async fn execute_tool_json(&self, tool_name: &str, args: Value) -> Result<Value>;
963
964 async fn execute_tool_dual(&self, tool_name: &str, args: Value) -> Result<SplitToolResult>;
965}
966
967#[async_trait]
968impl<Ctx> CanExecuteTool for Ctx
969where
970 Ctx: HasComponent<ExecuteComponent>
971 + HasComponent<LoggingComponent>
972 + HasComponent<CacheComponent>
973 + HasComponent<RetryComponent>
974 + Send
975 + Sync,
976 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
977 ComponentProvider<Ctx, LoggingComponent>: LoggingProvider<Ctx>,
978 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
979 ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
980{
981 async fn execute_tool_json(&self, tool_name: &str, args: Value) -> Result<Value> {
982 execute_tool_with_mode::<Ctx, JsonExecution>(self, tool_name, args).await
983 }
984
985 async fn execute_tool_dual(&self, tool_name: &str, args: Value) -> Result<SplitToolResult> {
986 execute_tool_with_mode::<Ctx, DualExecution>(self, tool_name, args).await
987 }
988}
989
990pub struct ToolFacade<Ctx> {
1000 ctx: Ctx,
1001}
1002
1003impl<Ctx> ToolFacade<Ctx> {
1004 pub fn new(ctx: Ctx) -> Self {
1005 Self { ctx }
1006 }
1007}
1008
1009#[async_trait]
1010impl<Ctx> Tool for ToolFacade<Ctx>
1011where
1012 Ctx: CanApproveTool + CanExecuteTool + CanProvideToolMetadata + Send + Sync + 'static,
1013{
1014 async fn execute(&self, args: Value) -> Result<Value> {
1015 self.ctx.approve_tool(self.name(), "execute").await?;
1016
1017 self.ctx.execute_tool_json(self.name(), args).await
1018 }
1019
1020 async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
1021 self.ctx.approve_tool(self.name(), "execute_dual").await?;
1022
1023 self.ctx.execute_tool_dual(self.name(), args).await
1024 }
1025
1026 fn name(&self) -> &str {
1027 CanProvideToolMetadata::tool_name(&self.ctx)
1028 }
1029
1030 fn description(&self) -> &str {
1031 CanProvideToolMetadata::tool_description(&self.ctx)
1032 }
1033
1034 fn parameter_schema(&self) -> Option<Value> {
1035 CanProvideToolMetadata::parameter_schema(&self.ctx)
1036 }
1037
1038 fn config_schema(&self) -> Option<Value> {
1039 CanProvideToolMetadata::config_schema(&self.ctx)
1040 }
1041
1042 fn state_schema(&self) -> Option<Value> {
1043 CanProvideToolMetadata::state_schema(&self.ctx)
1044 }
1045
1046 fn prompt_path(&self) -> Option<Cow<'static, str>> {
1047 CanProvideToolMetadata::prompt_path(&self.ctx)
1048 }
1049
1050 fn default_permission(&self) -> ToolPolicy {
1051 CanProvideToolMetadata::default_permission(&self.ctx)
1052 }
1053
1054 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
1055 CanProvideToolMetadata::allow_patterns(&self.ctx)
1056 }
1057
1058 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
1059 CanProvideToolMetadata::deny_patterns(&self.ctx)
1060 }
1061
1062 fn is_mutating(&self) -> bool {
1063 CanProvideToolMetadata::is_mutating(&self.ctx)
1064 }
1065
1066 fn is_parallel_safe(&self) -> bool {
1067 CanProvideToolMetadata::is_parallel_safe(&self.ctx)
1068 }
1069
1070 fn kind(&self) -> &'static str {
1071 CanProvideToolMetadata::tool_kind(&self.ctx)
1072 }
1073
1074 fn resource_hints(&self, args: &Value) -> Vec<String> {
1075 CanProvideToolMetadata::resource_hints(&self.ctx, args)
1076 }
1077
1078 fn execution_cost(&self) -> u8 {
1079 CanProvideToolMetadata::execution_cost(&self.ctx)
1080 }
1081}
1082
1083pub struct HandlerFacade<Ctx> {
1093 ctx: Ctx,
1094}
1095
1096impl<Ctx> HandlerFacade<Ctx> {
1097 pub fn new(ctx: Ctx) -> Self {
1098 Self { ctx }
1099 }
1100}
1101
1102#[async_trait]
1103impl<Ctx> ToolHandler for HandlerFacade<Ctx>
1104where
1105 Ctx: CanApproveTool + CanExecuteTool + Send + Sync + 'static,
1106{
1107 fn kind(&self) -> ToolKind {
1108 ToolKind::Function
1109 }
1110
1111 async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
1112 let args: Value = match &invocation.payload {
1114 ToolPayload::Function { arguments } => serde_json::from_str(arguments)
1115 .map_err(|e| ToolCallError::respond(format!("Invalid arguments: {e}")))?,
1116 _ => return Err(ToolCallError::respond("Unsupported payload type")),
1117 };
1118
1119 self.ctx
1120 .approve_tool(&invocation.tool_name, "handle")
1121 .await
1122 .map_err(|e| ToolCallError::respond(e.to_string()))?;
1123
1124 match self
1125 .ctx
1126 .execute_tool_json(&invocation.tool_name, args)
1127 .await
1128 {
1129 Ok(result) => {
1130 let text = value_to_text(&result);
1131 Ok(ToolOutput::simple(text))
1132 }
1133 Err(e) => Err(ToolCallError::Internal(e)),
1134 }
1135 }
1136}
1137
1138pub struct ComposableRuntime;
1148
1149impl ComposableRuntime {
1150 pub async fn run<Ctx>(ctx: &Ctx, tool_name: &str, description: &str) -> Result<()>
1154 where
1155 Ctx: CanApproveTool + Send + Sync,
1156 {
1157 ctx.approve_tool(tool_name, description).await?;
1158 Ok(())
1159 }
1160
1161 pub async fn run_with_sandbox<Ctx>(
1164 ctx: &Ctx,
1165 tool_name: &str,
1166 description: &str,
1167 ) -> Result<bool>
1168 where
1169 Ctx: CanApproveTool + CanResolveSandbox + Send + Sync,
1170 {
1171 ctx.approve_tool(tool_name, description).await?;
1172 Ok(ctx.sandbox_enabled())
1173 }
1174}
1175
1176pub struct InteractiveCtx {
1185 pub workspace_root: PathBuf,
1186}
1187
1188impl InteractiveCtx {
1189 pub fn new(workspace_root: PathBuf) -> Self {
1190 Self { workspace_root }
1191 }
1192}
1193
1194pub struct PromptApproval;
1201
1202#[async_trait]
1203impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for PromptApproval {
1204 async fn check_approval(_ctx: &Ctx, _tool_name: &str, _description: &str) -> Result<()> {
1205 Ok(())
1208 }
1209}
1210
1211pub trait HasWorkspaceRoot: Send + Sync {
1213 fn workspace_root(&self) -> &PathBuf;
1214}
1215
1216impl HasWorkspaceRoot for InteractiveCtx {
1217 fn workspace_root(&self) -> &PathBuf {
1218 &self.workspace_root
1219 }
1220}
1221
1222pub struct WorkspaceSandbox;
1227
1228#[async_trait]
1229impl<Ctx: HasWorkspaceRoot> SandboxProvider<Ctx> for WorkspaceSandbox {
1230 fn sandbox_enabled(_ctx: &Ctx) -> bool {
1231 true
1232 }
1233
1234 fn workspace_root(ctx: &Ctx) -> Option<&PathBuf> {
1235 Some(HasWorkspaceRoot::workspace_root(ctx))
1236 }
1237}
1238
1239delegate_components!(InteractiveCtx {
1240 ApprovalComponent => PromptApproval,
1241 SandboxComponent => WorkspaceSandbox,
1242 ExecuteComponent => PassthroughExecutor,
1243 MetadataComponent => PassthroughMetadata,
1244 LoggingComponent => TracingLogging,
1245 CacheComponent => NoCache,
1246 RetryComponent => NoRetry,
1247});
1248
1249pub struct CiCtx {
1251 pub workspace_root: PathBuf,
1252}
1253
1254impl CiCtx {
1255 pub fn new(workspace_root: PathBuf) -> Self {
1256 Self { workspace_root }
1257 }
1258}
1259
1260impl HasWorkspaceRoot for CiCtx {
1261 fn workspace_root(&self) -> &PathBuf {
1262 &self.workspace_root
1263 }
1264}
1265
1266pub type StrictWorkspaceSandbox = WorkspaceSandbox;
1268
1269delegate_components!(CiCtx {
1270 ApprovalComponent => AutoApproval,
1271 SandboxComponent => StrictWorkspaceSandbox,
1272 ExecuteComponent => PassthroughExecutor,
1273 MetadataComponent => PassthroughMetadata,
1274 LoggingComponent => NoLogging,
1275 CacheComponent => NoCache,
1276 RetryComponent => NoRetry,
1277});
1278
1279pub struct BenchCtx;
1281
1282delegate_components!(BenchCtx {
1283 ApprovalComponent => AutoApproval,
1284 SandboxComponent => NoSandbox,
1285 ExecuteComponent => PassthroughExecutor,
1286 MetadataComponent => PassthroughMetadata,
1287 LoggingComponent => NoLogging,
1288 CacheComponent => NoCache,
1289 RetryComponent => NoRetry,
1290});
1291
1292pub struct PassthroughExecutor;
1298
1299pub trait HasToolRef: Send + Sync {
1302 fn tool(&self) -> &dyn Tool;
1303}
1304
1305pub trait HasInnerTool: Send + Sync {
1310 fn inner_tool(&self) -> &dyn Tool;
1311}
1312
1313impl<Ctx> HasToolRef for Ctx
1314where
1315 Ctx: HasInnerTool + Send + Sync,
1316{
1317 fn tool(&self) -> &dyn Tool {
1318 self.inner_tool()
1319 }
1320}
1321
1322#[async_trait]
1323impl<Ctx: HasToolRef> ExecuteProvider<Ctx> for PassthroughExecutor {
1324 async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
1325 let tool = ctx.tool();
1326 tool.validate_args(&args)?;
1327 tool.execute(args).await
1328 }
1329
1330 async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
1331 let tool = ctx.tool();
1332 tool.validate_args(&args)?;
1333 tool.execute_dual(args).await
1334 }
1335}
1336
1337pub fn wrap_tool_interactive(
1350 tool: Arc<dyn Tool>,
1351 workspace_root: PathBuf,
1352) -> ToolFacade<ToolBridgeCtx<InteractiveCtx>> {
1353 let ctx = ToolBridgeCtx {
1354 inner: tool,
1355 runtime: InteractiveCtx::new(workspace_root),
1356 };
1357 ToolFacade::new(ctx)
1358}
1359
1360pub fn wrap_tool_ci(
1366 tool: Arc<dyn Tool>,
1367 workspace_root: PathBuf,
1368) -> ToolFacade<ToolBridgeCtx<CiCtx>> {
1369 let ctx = ToolBridgeCtx {
1370 inner: tool,
1371 runtime: CiCtx::new(workspace_root),
1372 };
1373 ToolFacade::new(ctx)
1374}
1375
1376pub struct ToolBridgeCtx<Runtime> {
1383 inner: Arc<dyn Tool>,
1384 runtime: Runtime,
1388}
1389
1390impl<Runtime: HasWorkspaceRoot> HasWorkspaceRoot for ToolBridgeCtx<Runtime> {
1391 fn workspace_root(&self) -> &PathBuf {
1392 self.runtime.workspace_root()
1393 }
1394}
1395
1396impl<Runtime: Send + Sync> HasInnerTool for ToolBridgeCtx<Runtime> {
1397 fn inner_tool(&self) -> &dyn Tool {
1398 self.inner.as_ref()
1399 }
1400}
1401
1402impl<Name, Runtime> HasComponent<Name> for ToolBridgeCtx<Runtime>
1404where
1405 Runtime: HasComponent<Name>,
1406{
1407 type Provider = ComponentProvider<Runtime, Name>;
1408}
1409
1410pub trait HasToolInstance<T>: Send + Sync {
1412 fn tool_instance(&self) -> &T;
1413}
1414
1415pub struct TypedToolExecutor<T>(PhantomData<T>);
1417
1418#[async_trait]
1419impl<Ctx, T> ExecuteProvider<Ctx> for TypedToolExecutor<T>
1420where
1421 Ctx: HasToolRef + Send + Sync,
1422 T: Tool + Send + Sync,
1423{
1424 async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
1425 <PassthroughExecutor as ExecuteProvider<Ctx>>::execute(ctx, args).await
1426 }
1427
1428 async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
1429 <PassthroughExecutor as ExecuteProvider<Ctx>>::execute_dual(ctx, args).await
1430 }
1431}
1432
1433pub struct TypedToolMetadata<T>(PhantomData<T>);
1435
1436impl<Ctx, T> MetadataProvider<Ctx> for TypedToolMetadata<T>
1437where
1438 Ctx: HasToolRef,
1439 T: Tool + Send + Sync,
1440{
1441 fn tool_name(ctx: &Ctx) -> &str {
1442 <PassthroughMetadata as MetadataProvider<Ctx>>::tool_name(ctx)
1443 }
1444
1445 fn tool_description(ctx: &Ctx) -> &str {
1446 <PassthroughMetadata as MetadataProvider<Ctx>>::tool_description(ctx)
1447 }
1448
1449 fn parameter_schema(ctx: &Ctx) -> Option<Value> {
1450 <PassthroughMetadata as MetadataProvider<Ctx>>::parameter_schema(ctx)
1451 }
1452
1453 fn config_schema(ctx: &Ctx) -> Option<Value> {
1454 <PassthroughMetadata as MetadataProvider<Ctx>>::config_schema(ctx)
1455 }
1456
1457 fn state_schema(ctx: &Ctx) -> Option<Value> {
1458 <PassthroughMetadata as MetadataProvider<Ctx>>::state_schema(ctx)
1459 }
1460
1461 fn prompt_path(ctx: &Ctx) -> Option<Cow<'static, str>> {
1462 <PassthroughMetadata as MetadataProvider<Ctx>>::prompt_path(ctx)
1463 }
1464
1465 fn default_permission(ctx: &Ctx) -> ToolPolicy {
1466 <PassthroughMetadata as MetadataProvider<Ctx>>::default_permission(ctx)
1467 }
1468
1469 fn allow_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
1470 <PassthroughMetadata as MetadataProvider<Ctx>>::allow_patterns(ctx)
1471 }
1472
1473 fn deny_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
1474 <PassthroughMetadata as MetadataProvider<Ctx>>::deny_patterns(ctx)
1475 }
1476
1477 fn is_mutating(ctx: &Ctx) -> bool {
1478 <PassthroughMetadata as MetadataProvider<Ctx>>::is_mutating(ctx)
1479 }
1480
1481 fn is_parallel_safe(ctx: &Ctx) -> bool {
1482 <PassthroughMetadata as MetadataProvider<Ctx>>::is_parallel_safe(ctx)
1483 }
1484
1485 fn tool_kind(ctx: &Ctx) -> &'static str {
1486 <PassthroughMetadata as MetadataProvider<Ctx>>::tool_kind(ctx)
1487 }
1488
1489 fn resource_hints(ctx: &Ctx, args: &Value) -> Vec<String> {
1490 <PassthroughMetadata as MetadataProvider<Ctx>>::resource_hints(ctx, args)
1491 }
1492
1493 fn execution_cost(ctx: &Ctx) -> u8 {
1494 <PassthroughMetadata as MetadataProvider<Ctx>>::execution_cost(ctx)
1495 }
1496}
1497
1498pub struct TypedToolCtx<Runtime, T> {
1500 tool: T,
1501 runtime: Runtime,
1502}
1503
1504impl<Runtime, T> TypedToolCtx<Runtime, T> {
1505 pub fn new(tool: T, runtime: Runtime) -> Self {
1506 Self { tool, runtime }
1507 }
1508}
1509
1510impl<Runtime: HasWorkspaceRoot + Send + Sync, T: Send + Sync> HasWorkspaceRoot
1511 for TypedToolCtx<Runtime, T>
1512{
1513 fn workspace_root(&self) -> &PathBuf {
1514 self.runtime.workspace_root()
1515 }
1516}
1517
1518impl<Runtime: Send + Sync, T: Send + Sync> HasToolInstance<T> for TypedToolCtx<Runtime, T> {
1519 fn tool_instance(&self) -> &T {
1520 &self.tool
1521 }
1522}
1523
1524impl<Runtime: Send + Sync, T> HasToolRef for TypedToolCtx<Runtime, T>
1525where
1526 T: Tool + Send + Sync,
1527{
1528 fn tool(&self) -> &dyn Tool {
1529 &self.tool
1530 }
1531}
1532
1533macro_rules! delegate_runtime_components_for_typed_ctx {
1534 ($($component:ty),+ $(,)?) => {
1535 $(
1536 impl<Runtime, T> HasComponent<$component> for TypedToolCtx<Runtime, T>
1537 where
1538 Runtime: HasComponent<$component>,
1539 {
1540 type Provider = ComponentProvider<Runtime, $component>;
1541 }
1542 )+
1543 };
1544}
1545
1546delegate_runtime_components_for_typed_ctx!(
1547 ApprovalComponent,
1548 SandboxComponent,
1549 SessionComponent,
1550 OutputMapComponent,
1551 LoggingComponent,
1552 CacheComponent,
1553 RetryComponent,
1554);
1555
1556impl<Runtime, T> HasComponent<ExecuteComponent> for TypedToolCtx<Runtime, T>
1557where
1558 Runtime: Send + Sync,
1559 T: Tool + Send + Sync,
1560{
1561 type Provider = TypedToolExecutor<T>;
1562}
1563
1564impl<Runtime, T> HasComponent<MetadataComponent> for TypedToolCtx<Runtime, T>
1565where
1566 Runtime: Send + Sync,
1567 T: Tool + Send + Sync,
1568{
1569 type Provider = TypedToolMetadata<T>;
1570}
1571
1572pub fn wrap_native_tool_interactive<T>(
1578 tool: T,
1579 workspace_root: PathBuf,
1580) -> ToolFacade<TypedToolCtx<InteractiveCtx, T>>
1581where
1582 T: Tool + Send + Sync + 'static,
1583{
1584 ToolFacade::new(TypedToolCtx::new(tool, InteractiveCtx::new(workspace_root)))
1585}
1586
1587pub fn wrap_native_tool_ci<T>(
1593 tool: T,
1594 workspace_root: PathBuf,
1595) -> ToolFacade<TypedToolCtx<CiCtx, T>>
1596where
1597 T: Tool + Send + Sync + 'static,
1598{
1599 ToolFacade::new(TypedToolCtx::new(tool, CiCtx::new(workspace_root)))
1600}
1601
1602#[cfg(test)]
1603mod tests;