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 use vtcode_commons::cgp::{ComponentProvider, HasComponent};
86pub use vtcode_commons::delegate_components;
87
88pub enum ApprovalComponent {}
94
95pub enum SandboxComponent {}
97
98pub enum ExecuteComponent {}
100
101pub enum MetadataComponent {}
103
104pub enum SessionComponent {}
106
107pub enum OutputMapComponent {}
109
110pub enum LoggingComponent {}
112
113pub enum CacheComponent {}
115
116pub enum RetryComponent {}
118
119#[async_trait]
129pub trait ApprovalProvider<Ctx: Send + Sync>: Send + Sync {
130 async fn check_approval(ctx: &Ctx, tool_name: &str, description: &str) -> Result<()>;
133}
134
135pub trait SandboxProvider<Ctx: Send + Sync>: Send + Sync {
137 fn sandbox_enabled(ctx: &Ctx) -> bool;
139
140 fn workspace_root(ctx: &Ctx) -> Option<&PathBuf>;
142}
143
144pub trait MetadataProvider<Ctx>: Send + Sync {
146 fn tool_name(_ctx: &Ctx) -> &str {
147 "unknown"
148 }
149
150 fn tool_description(_ctx: &Ctx) -> &str {
151 ""
152 }
153
154 fn parameter_schema(_ctx: &Ctx) -> Option<Value> {
155 None
156 }
157
158 fn config_schema(_ctx: &Ctx) -> Option<Value> {
159 None
160 }
161
162 fn state_schema(_ctx: &Ctx) -> Option<Value> {
163 None
164 }
165
166 fn prompt_path(_ctx: &Ctx) -> Option<Cow<'static, str>> {
167 None
168 }
169
170 fn default_permission(_ctx: &Ctx) -> ToolPolicy {
171 ToolPolicy::Prompt
172 }
173
174 fn allow_patterns(_ctx: &Ctx) -> Option<&'static [&'static str]> {
175 None
176 }
177
178 fn deny_patterns(_ctx: &Ctx) -> Option<&'static [&'static str]> {
179 None
180 }
181
182 fn is_mutating(_ctx: &Ctx) -> bool {
183 true
184 }
185
186 fn is_parallel_safe(ctx: &Ctx) -> bool {
187 !Self::is_mutating(ctx)
188 }
189
190 fn tool_kind(_ctx: &Ctx) -> &'static str {
191 "unknown"
192 }
193
194 fn resource_hints(_ctx: &Ctx, _args: &Value) -> Vec<String> {
195 Vec::new()
196 }
197
198 fn execution_cost(_ctx: &Ctx) -> u8 {
199 5
200 }
201}
202
203pub trait OutputMapProvider<Ctx>: Send + Sync {
205 type Input;
206 type Output;
207
208 fn map_output(ctx: &Ctx, input: Self::Input) -> Self::Output;
209}
210
211#[async_trait]
217pub trait ExecuteProvider<Ctx: Send + Sync>: Send + Sync {
218 async fn execute(ctx: &Ctx, args: Value) -> Result<Value>;
220
221 async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
225 let result = Self::execute(ctx, args).await?;
226 let name = if let Some(n) = result.get("tool_name").and_then(|v| v.as_str()) {
227 n.to_string()
228 } else {
229 "unknown".to_string()
230 };
231 let content = value_to_text(&result);
232 Ok(SplitToolResult::simple(&name, content))
233 }
234}
235
236pub trait LoggingProvider<Ctx>: Send + Sync {
238 fn on_start(_ctx: &Ctx, _tool_name: &str, _args: &Value) {}
239
240 fn on_cache_hit(_ctx: &Ctx, _tool_name: &str, _args: &Value) {}
241
242 fn on_success(_ctx: &Ctx, _tool_name: &str, _duration: Duration, _attempt: u32, _from_cache: bool) {}
243
244 fn on_retry(_ctx: &Ctx, _tool_name: &str, _next_attempt: u32, _backoff: Duration, _error: &Error) {}
245
246 fn on_failure(_ctx: &Ctx, _tool_name: &str, _duration: Duration, _attempt: u32, _error: &Error) {}
247}
248
249pub trait CacheProvider<Ctx>: Send + Sync {
251 fn get_json(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> Option<Value> {
252 None
253 }
254
255 fn put_json(_ctx: &Ctx, _tool_name: &str, _args: &Value, _result: &Value) {}
256
257 fn get_dual(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> Option<SplitToolResult> {
258 None
259 }
260
261 fn put_dual(_ctx: &Ctx, _tool_name: &str, _args: &Value, _result: &SplitToolResult) {}
262}
263
264pub trait RetryProvider<Ctx>: Send + Sync {
266 fn max_attempts(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> u32 {
267 1
268 }
269
270 fn should_retry(_ctx: &Ctx, _tool_name: &str, _attempt: u32, _error: &Error) -> bool {
271 false
272 }
273
274 fn backoff_duration(_ctx: &Ctx, _tool_name: &str, _attempt: u32) -> Duration {
275 Duration::ZERO
276 }
277}
278
279pub struct AutoApproval;
285
286#[async_trait]
287impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for AutoApproval {
288 async fn check_approval(_ctx: &Ctx, _tool_name: &str, _description: &str) -> Result<()> {
289 Ok(())
290 }
291}
292
293pub struct DenyAllApproval;
295
296#[async_trait]
297impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for DenyAllApproval {
298 async fn check_approval(_ctx: &Ctx, tool_name: &str, _description: &str) -> Result<()> {
299 anyhow::bail!("operation denied: {tool_name} is not permitted in this context")
300 }
301}
302
303pub struct NoSandbox;
305
306#[async_trait]
307impl<Ctx: Send + Sync> SandboxProvider<Ctx> for NoSandbox {
308 fn sandbox_enabled(_ctx: &Ctx) -> bool {
309 false
310 }
311
312 fn workspace_root(_ctx: &Ctx) -> Option<&PathBuf> {
313 None
314 }
315}
316
317pub struct DefaultMetadata;
319
320impl<Ctx> MetadataProvider<Ctx> for DefaultMetadata {}
321
322pub struct NoLogging;
324
325impl<Ctx> LoggingProvider<Ctx> for NoLogging {}
326
327pub struct NoCache;
329
330impl<Ctx> CacheProvider<Ctx> for NoCache {}
331
332pub struct NoRetry;
334
335impl<Ctx> RetryProvider<Ctx> for NoRetry {}
336
337pub struct TracingLogging;
339
340impl<Ctx> LoggingProvider<Ctx> for TracingLogging {
341 fn on_start(_ctx: &Ctx, tool_name: &str, _args: &Value) {
342 tracing::trace!(tool = %tool_name, "CGP tool execution started");
343 }
344
345 fn on_cache_hit(_ctx: &Ctx, tool_name: &str, _args: &Value) {
346 tracing::debug!(tool = %tool_name, "CGP tool result served from cache");
347 }
348
349 fn on_success(_ctx: &Ctx, tool_name: &str, duration: Duration, attempt: u32, from_cache: bool) {
350 tracing::trace!(
351 tool = %tool_name,
352 duration_ms = duration.as_millis() as u64,
353 attempt,
354 from_cache,
355 "CGP tool execution succeeded"
356 );
357 }
358
359 fn on_retry(_ctx: &Ctx, tool_name: &str, next_attempt: u32, backoff: Duration, error: &Error) {
360 tracing::debug!(
361 tool = %tool_name,
362 next_attempt,
363 backoff_ms = backoff.as_millis() as u64,
364 error = %error,
365 "CGP tool execution retry scheduled"
366 );
367 }
368
369 fn on_failure(_ctx: &Ctx, tool_name: &str, duration: Duration, attempt: u32, error: &Error) {
370 tracing::warn!(
371 tool = %tool_name,
372 duration_ms = duration.as_millis() as u64,
373 attempt,
374 error = %error,
375 "CGP tool execution failed"
376 );
377 }
378}
379
380pub use crate::retry::RetryPolicy;
383
384pub trait HasRetryPolicy: Send + Sync {
386 fn retry_policy(&self) -> RetryPolicy;
387}
388
389pub struct ExponentialBackoffRetry;
394
395impl<Ctx: HasRetryPolicy> RetryProvider<Ctx> for ExponentialBackoffRetry {
396 fn max_attempts(ctx: &Ctx, _tool_name: &str, _args: &Value) -> u32 {
397 ctx.retry_policy().max_attempts.max(1)
398 }
399
400 fn should_retry(_ctx: &Ctx, _tool_name: &str, _attempt: u32, error: &Error) -> bool {
401 vtcode_commons::detect_misconfiguration_in_anyhow(error).is_none()
402 }
403
404 fn backoff_duration(ctx: &Ctx, _tool_name: &str, attempt: u32) -> Duration {
405 let policy = ctx.retry_policy();
406 policy.delay_for_attempt(attempt.saturating_sub(1))
408 }
409}
410
411#[derive(Debug, Clone, Hash, PartialEq, Eq)]
413pub struct ToolExecutionCacheKey(String);
414
415impl CacheKey for ToolExecutionCacheKey {
416 fn to_cache_key(&self) -> String {
417 self.0.clone()
418 }
419}
420
421pub trait HasExecutionCaches: Send + Sync {
423 fn json_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, Value>;
424 fn dual_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, SplitToolResult>;
425}
426
427fn hash_json_value<H: Hasher>(hasher: &mut H, value: &Value) {
431 match value {
432 Value::Null => hasher.write_u8(0x00),
433 Value::Bool(b) => {
434 hasher.write_u8(0x01);
435 hasher.write_u8(u8::from(*b));
436 }
437 Value::Number(n) => {
438 hasher.write_u8(0x02);
439 let s = n.to_string();
440 hasher.write(s.as_bytes());
441 }
442 Value::String(s) => {
443 hasher.write_u8(0x03);
444 hasher.write(s.as_bytes());
445 }
446 Value::Array(arr) => {
447 hasher.write_u8(0x04);
448 hasher.write_usize(arr.len());
449 for item in arr {
450 hash_json_value(hasher, item);
451 }
452 }
453 Value::Object(map) => {
454 hasher.write_u8(0x05);
455 hasher.write_usize(map.len());
456 let mut keys: Vec<&String> = map.keys().collect();
458 keys.sort();
459 for k in keys {
460 hasher.write(k.as_bytes());
461 #[allow(
462 clippy::expect_used,
463 reason = "Intentional compatibility, platform, or test-only suppression."
464 )]
465 let value = map.get(k).expect("key from map.keys() must exist in map");
466 hash_json_value(hasher, value);
467 }
468 }
469 }
470}
471
472fn build_execution_cache_key(tool_name: &str, args: &Value) -> ToolExecutionCacheKey {
473 let mut hasher = crate::core::agent::hash_utils::StableHasher::new();
474 hasher.write(tool_name.as_bytes());
475 hash_json_value(&mut hasher, args);
476
477 let mut cache_key = String::with_capacity(tool_name.len() + 22);
478 cache_key.push_str(tool_name);
479 cache_key.push_str("::");
480 let _ = write!(&mut cache_key, "{}", hasher.finish());
481
482 ToolExecutionCacheKey(cache_key)
483}
484
485pub struct CachedResults;
487
488impl<Ctx: HasExecutionCaches> CacheProvider<Ctx> for CachedResults {
489 fn get_json(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Value> {
490 ctx.json_cache().get_owned(&build_execution_cache_key(tool_name, args))
491 }
492
493 fn put_json(ctx: &Ctx, tool_name: &str, args: &Value, result: &Value) {
494 let key = build_execution_cache_key(tool_name, args);
495 let size = estimate_json_size(result);
496 ctx.json_cache().insert(key, result.clone(), size);
497 }
498
499 fn get_dual(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<SplitToolResult> {
500 ctx.dual_cache().get_owned(&build_execution_cache_key(tool_name, args))
501 }
502
503 fn put_dual(ctx: &Ctx, tool_name: &str, args: &Value, result: &SplitToolResult) {
504 let key = build_execution_cache_key(tool_name, args);
505 let size = (result.llm_content.len() + result.ui_content.len()) as u64;
506 ctx.dual_cache().insert(key, result.clone(), size);
507 }
508}
509
510pub struct PassthroughMetadata;
512
513impl<Ctx: HasToolRef> MetadataProvider<Ctx> for PassthroughMetadata {
514 fn tool_name(ctx: &Ctx) -> &str {
515 ctx.tool().name()
516 }
517
518 fn tool_description(ctx: &Ctx) -> &str {
519 ctx.tool().description()
520 }
521
522 fn parameter_schema(ctx: &Ctx) -> Option<Value> {
523 ctx.tool().parameter_schema()
524 }
525
526 fn config_schema(ctx: &Ctx) -> Option<Value> {
527 ctx.tool().config_schema()
528 }
529
530 fn state_schema(ctx: &Ctx) -> Option<Value> {
531 ctx.tool().state_schema()
532 }
533
534 fn prompt_path(ctx: &Ctx) -> Option<Cow<'static, str>> {
535 ctx.tool().prompt_path()
536 }
537
538 fn default_permission(ctx: &Ctx) -> ToolPolicy {
539 ctx.tool().default_permission()
540 }
541
542 fn allow_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
543 ctx.tool().allow_patterns()
544 }
545
546 fn deny_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
547 ctx.tool().deny_patterns()
548 }
549
550 fn is_mutating(ctx: &Ctx) -> bool {
551 ctx.tool().is_mutating()
552 }
553
554 fn is_parallel_safe(ctx: &Ctx) -> bool {
555 ctx.tool().is_parallel_safe()
556 }
557
558 fn tool_kind(ctx: &Ctx) -> &'static str {
559 ctx.tool().kind()
560 }
561
562 fn resource_hints(ctx: &Ctx, args: &Value) -> Vec<String> {
563 ctx.tool().resource_hints(args)
564 }
565
566 fn execution_cost(ctx: &Ctx) -> u8 {
567 ctx.tool().execution_cost()
568 }
569}
570
571fn value_to_text(value: &Value) -> String {
576 if value.is_string() {
577 value.as_str().unwrap_or("").to_string()
578 } else {
579 json_to_string_pretty(value)
580 }
581}
582
583#[async_trait]
584trait ExecutionMode<Ctx>
585where
586 Ctx: HasComponent<ExecuteComponent> + HasComponent<CacheComponent> + Send + Sync,
587 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
588 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
589{
590 type Output: Send;
591
592 fn get_cached(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Self::Output>;
593
594 fn put_cached(ctx: &Ctx, tool_name: &str, args: &Value, result: &Self::Output);
595
596 async fn execute(ctx: &Ctx, args: Value) -> Result<Self::Output>;
597}
598
599struct JsonExecution;
600
601#[async_trait]
602impl<Ctx> ExecutionMode<Ctx> for JsonExecution
603where
604 Ctx: HasComponent<ExecuteComponent> + HasComponent<CacheComponent> + Send + Sync,
605 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
606 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
607{
608 type Output = Value;
609
610 fn get_cached(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Self::Output> {
611 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::get_json(ctx, tool_name, args)
612 }
613
614 fn put_cached(ctx: &Ctx, tool_name: &str, args: &Value, result: &Self::Output) {
615 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::put_json(ctx, tool_name, args, result);
616 }
617
618 async fn execute(ctx: &Ctx, args: Value) -> Result<Self::Output> {
619 <ComponentProvider<Ctx, ExecuteComponent> as ExecuteProvider<Ctx>>::execute(ctx, args).await
620 }
621}
622
623struct DualExecution;
624
625#[async_trait]
626impl<Ctx> ExecutionMode<Ctx> for DualExecution
627where
628 Ctx: HasComponent<ExecuteComponent> + HasComponent<CacheComponent> + Send + Sync,
629 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
630 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
631{
632 type Output = SplitToolResult;
633
634 fn get_cached(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Self::Output> {
635 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::get_dual(ctx, tool_name, args)
636 }
637
638 fn put_cached(ctx: &Ctx, tool_name: &str, args: &Value, result: &Self::Output) {
639 <ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::put_dual(ctx, tool_name, args, result);
640 }
641
642 async fn execute(ctx: &Ctx, args: Value) -> Result<Self::Output> {
643 <ComponentProvider<Ctx, ExecuteComponent> as ExecuteProvider<Ctx>>::execute_dual(ctx, args).await
644 }
645}
646
647fn retry_backoff<Ctx>(ctx: &Ctx, tool_name: &str, attempt: u32) -> Duration
648where
649 Ctx: HasComponent<RetryComponent> + Send + Sync,
650 ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
651{
652 <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::backoff_duration(ctx, tool_name, attempt)
653 .min(MAX_RETRY_BACKOFF)
654}
655
656async fn execute_tool_with_mode<Ctx, Mode>(ctx: &Ctx, tool_name: &str, args: Value) -> Result<Mode::Output>
657where
658 Ctx: HasComponent<ExecuteComponent>
659 + HasComponent<LoggingComponent>
660 + HasComponent<CacheComponent>
661 + HasComponent<RetryComponent>
662 + Send
663 + Sync,
664 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
665 ComponentProvider<Ctx, LoggingComponent>: LoggingProvider<Ctx>,
666 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
667 ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
668 Mode: ExecutionMode<Ctx>,
669{
670 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_start(ctx, tool_name, &args);
671
672 if let Some(result) = Mode::get_cached(ctx, tool_name, &args) {
673 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_cache_hit(ctx, tool_name, &args);
674 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
675 ctx,
676 tool_name,
677 Duration::ZERO,
678 1,
679 true,
680 );
681 return Ok(result);
682 }
683
684 let started = Instant::now();
685 let max_attempts =
686 <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::max_attempts(ctx, tool_name, &args)
687 .clamp(1, MAX_RETRY_ATTEMPTS);
688
689 let mut attempt = 1;
690 loop {
691 match Mode::execute(ctx, args.clone()).await {
692 Ok(result) => {
693 Mode::put_cached(ctx, tool_name, &args, &result);
694 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
695 ctx,
696 tool_name,
697 started.elapsed(),
698 attempt,
699 false,
700 );
701 return Ok(result);
702 }
703 Err(error) => {
704 let should_retry = attempt < max_attempts
705 && <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::should_retry(
706 ctx, tool_name, attempt, &error,
707 );
708
709 if !should_retry {
710 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_failure(
711 ctx,
712 tool_name,
713 started.elapsed(),
714 attempt,
715 &error,
716 );
717 return Err(error);
718 }
719
720 let backoff = retry_backoff(ctx, tool_name, attempt);
721 <ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_retry(
722 ctx,
723 tool_name,
724 attempt + 1,
725 backoff,
726 &error,
727 );
728 if !backoff.is_zero() {
729 tokio::time::sleep(backoff).await;
730 }
731 attempt += 1;
732 }
733 }
734 }
735}
736
737#[async_trait]
738pub trait CanApproveTool: Send + Sync {
739 async fn approve_tool(&self, tool_name: &str, description: &str) -> Result<()>;
740}
741
742#[async_trait]
743impl<Ctx> CanApproveTool for Ctx
744where
745 Ctx: HasComponent<ApprovalComponent> + Send + Sync,
746 ComponentProvider<Ctx, ApprovalComponent>: ApprovalProvider<Ctx>,
747{
748 async fn approve_tool(&self, tool_name: &str, description: &str) -> Result<()> {
749 <ComponentProvider<Ctx, ApprovalComponent> as ApprovalProvider<Ctx>>::check_approval(
750 self,
751 tool_name,
752 description,
753 )
754 .await
755 }
756}
757
758pub trait CanResolveSandbox: Send + Sync {
759 fn sandbox_enabled(&self) -> bool;
760
761 fn workspace_root(&self) -> Option<&PathBuf>;
762}
763
764impl<Ctx> CanResolveSandbox for Ctx
765where
766 Ctx: HasComponent<SandboxComponent> + Send + Sync,
767 ComponentProvider<Ctx, SandboxComponent>: SandboxProvider<Ctx>,
768{
769 fn sandbox_enabled(&self) -> bool {
770 <ComponentProvider<Ctx, SandboxComponent> as SandboxProvider<Ctx>>::sandbox_enabled(self)
771 }
772
773 fn workspace_root(&self) -> Option<&PathBuf> {
774 <ComponentProvider<Ctx, SandboxComponent> as SandboxProvider<Ctx>>::workspace_root(self)
775 }
776}
777
778pub trait CanProvideToolMetadata: Send + Sync {
779 fn tool_name(&self) -> &str;
780
781 fn tool_description(&self) -> &str;
782
783 fn parameter_schema(&self) -> Option<Value>;
784
785 fn config_schema(&self) -> Option<Value>;
786
787 fn state_schema(&self) -> Option<Value>;
788
789 fn prompt_path(&self) -> Option<Cow<'static, str>>;
790
791 fn default_permission(&self) -> ToolPolicy;
792
793 fn allow_patterns(&self) -> Option<&'static [&'static str]>;
794
795 fn deny_patterns(&self) -> Option<&'static [&'static str]>;
796
797 fn is_mutating(&self) -> bool;
798
799 fn is_parallel_safe(&self) -> bool;
800
801 fn tool_kind(&self) -> &'static str;
802
803 fn resource_hints(&self, args: &Value) -> Vec<String>;
804
805 fn execution_cost(&self) -> u8;
806}
807
808impl<Ctx> CanProvideToolMetadata for Ctx
809where
810 Ctx: HasComponent<MetadataComponent> + Send + Sync,
811 ComponentProvider<Ctx, MetadataComponent>: MetadataProvider<Ctx>,
812{
813 fn tool_name(&self) -> &str {
814 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_name(self)
815 }
816
817 fn tool_description(&self) -> &str {
818 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_description(self)
819 }
820
821 fn parameter_schema(&self) -> Option<Value> {
822 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::parameter_schema(self)
823 }
824
825 fn config_schema(&self) -> Option<Value> {
826 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::config_schema(self)
827 }
828
829 fn state_schema(&self) -> Option<Value> {
830 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::state_schema(self)
831 }
832
833 fn prompt_path(&self) -> Option<Cow<'static, str>> {
834 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::prompt_path(self)
835 }
836
837 fn default_permission(&self) -> ToolPolicy {
838 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::default_permission(self)
839 }
840
841 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
842 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::allow_patterns(self)
843 }
844
845 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
846 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::deny_patterns(self)
847 }
848
849 fn is_mutating(&self) -> bool {
850 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::is_mutating(self)
851 }
852
853 fn is_parallel_safe(&self) -> bool {
854 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::is_parallel_safe(self)
855 }
856
857 fn tool_kind(&self) -> &'static str {
858 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_kind(self)
859 }
860
861 fn resource_hints(&self, args: &Value) -> Vec<String> {
862 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::resource_hints(self, args)
863 }
864
865 fn execution_cost(&self) -> u8 {
866 <ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::execution_cost(self)
867 }
868}
869
870#[async_trait]
871pub trait CanExecuteTool: Send + Sync {
872 async fn execute_tool_json(&self, tool_name: &str, args: Value) -> Result<Value>;
873
874 async fn execute_tool_dual(&self, tool_name: &str, args: Value) -> Result<SplitToolResult>;
875}
876
877#[async_trait]
878impl<Ctx> CanExecuteTool for Ctx
879where
880 Ctx: HasComponent<ExecuteComponent>
881 + HasComponent<LoggingComponent>
882 + HasComponent<CacheComponent>
883 + HasComponent<RetryComponent>
884 + Send
885 + Sync,
886 ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
887 ComponentProvider<Ctx, LoggingComponent>: LoggingProvider<Ctx>,
888 ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
889 ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
890{
891 async fn execute_tool_json(&self, tool_name: &str, args: Value) -> Result<Value> {
892 execute_tool_with_mode::<Ctx, JsonExecution>(self, tool_name, args).await
893 }
894
895 async fn execute_tool_dual(&self, tool_name: &str, args: Value) -> Result<SplitToolResult> {
896 execute_tool_with_mode::<Ctx, DualExecution>(self, tool_name, args).await
897 }
898}
899
900pub struct ToolFacade<Ctx> {
910 ctx: Ctx,
911}
912
913impl<Ctx> ToolFacade<Ctx> {
914 pub fn new(ctx: Ctx) -> Self {
915 Self { ctx }
916 }
917}
918
919#[async_trait]
920impl<Ctx> Tool for ToolFacade<Ctx>
921where
922 Ctx: CanApproveTool + CanExecuteTool + CanProvideToolMetadata + Send + Sync + 'static,
923{
924 async fn execute(&self, args: Value) -> Result<Value> {
925 self.ctx.approve_tool(self.name(), "execute").await?;
926
927 self.ctx.execute_tool_json(self.name(), args).await
928 }
929
930 async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
931 self.ctx.approve_tool(self.name(), "execute_dual").await?;
932
933 self.ctx.execute_tool_dual(self.name(), args).await
934 }
935
936 fn name(&self) -> &str {
937 CanProvideToolMetadata::tool_name(&self.ctx)
938 }
939
940 fn description(&self) -> &str {
941 CanProvideToolMetadata::tool_description(&self.ctx)
942 }
943
944 fn parameter_schema(&self) -> Option<Value> {
945 CanProvideToolMetadata::parameter_schema(&self.ctx)
946 }
947
948 fn config_schema(&self) -> Option<Value> {
949 CanProvideToolMetadata::config_schema(&self.ctx)
950 }
951
952 fn state_schema(&self) -> Option<Value> {
953 CanProvideToolMetadata::state_schema(&self.ctx)
954 }
955
956 fn prompt_path(&self) -> Option<Cow<'static, str>> {
957 CanProvideToolMetadata::prompt_path(&self.ctx)
958 }
959
960 fn default_permission(&self) -> ToolPolicy {
961 CanProvideToolMetadata::default_permission(&self.ctx)
962 }
963
964 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
965 CanProvideToolMetadata::allow_patterns(&self.ctx)
966 }
967
968 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
969 CanProvideToolMetadata::deny_patterns(&self.ctx)
970 }
971
972 fn is_mutating(&self) -> bool {
973 CanProvideToolMetadata::is_mutating(&self.ctx)
974 }
975
976 fn is_parallel_safe(&self) -> bool {
977 CanProvideToolMetadata::is_parallel_safe(&self.ctx)
978 }
979
980 fn kind(&self) -> &'static str {
981 CanProvideToolMetadata::tool_kind(&self.ctx)
982 }
983
984 fn resource_hints(&self, args: &Value) -> Vec<String> {
985 CanProvideToolMetadata::resource_hints(&self.ctx, args)
986 }
987
988 fn execution_cost(&self) -> u8 {
989 CanProvideToolMetadata::execution_cost(&self.ctx)
990 }
991}
992
993pub struct HandlerFacade<Ctx> {
1003 ctx: Ctx,
1004}
1005
1006impl<Ctx> HandlerFacade<Ctx> {
1007 pub fn new(ctx: Ctx) -> Self {
1008 Self { ctx }
1009 }
1010}
1011
1012#[async_trait]
1013impl<Ctx> ToolHandler for HandlerFacade<Ctx>
1014where
1015 Ctx: CanApproveTool + CanExecuteTool + Send + Sync + 'static,
1016{
1017 fn kind(&self) -> ToolKind {
1018 ToolKind::Function
1019 }
1020
1021 async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
1022 let args: Value = match &invocation.payload {
1024 ToolPayload::Function { arguments } => serde_json::from_str(arguments)
1025 .map_err(|e| ToolCallError::respond(format!("Invalid arguments: {e}")))?,
1026 _ => return Err(ToolCallError::respond("Unsupported payload type")),
1027 };
1028
1029 self.ctx
1030 .approve_tool(&invocation.tool_name, "handle")
1031 .await
1032 .map_err(|e| ToolCallError::respond(e.to_string()))?;
1033
1034 match self.ctx.execute_tool_json(&invocation.tool_name, args).await {
1035 Ok(result) => {
1036 let text = value_to_text(&result);
1037 Ok(ToolOutput::simple(text))
1038 }
1039 Err(e) => Err(ToolCallError::Internal(e)),
1040 }
1041 }
1042}
1043
1044pub struct ComposableRuntime;
1054
1055impl ComposableRuntime {
1056 pub async fn run<Ctx>(ctx: &Ctx, tool_name: &str, description: &str) -> Result<()>
1060 where
1061 Ctx: CanApproveTool + Send + Sync,
1062 {
1063 ctx.approve_tool(tool_name, description).await?;
1064 Ok(())
1065 }
1066
1067 pub async fn run_with_sandbox<Ctx>(ctx: &Ctx, tool_name: &str, description: &str) -> Result<bool>
1070 where
1071 Ctx: CanApproveTool + CanResolveSandbox + Send + Sync,
1072 {
1073 ctx.approve_tool(tool_name, description).await?;
1074 Ok(ctx.sandbox_enabled())
1075 }
1076}
1077
1078pub struct InteractiveCtx {
1087 pub workspace_root: PathBuf,
1088}
1089
1090impl InteractiveCtx {
1091 pub fn new(workspace_root: PathBuf) -> Self {
1092 Self { workspace_root }
1093 }
1094}
1095
1096pub struct PromptApproval;
1103
1104#[async_trait]
1105impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for PromptApproval {
1106 async fn check_approval(_ctx: &Ctx, _tool_name: &str, _description: &str) -> Result<()> {
1107 Ok(())
1110 }
1111}
1112
1113pub trait HasWorkspaceRoot: Send + Sync {
1115 fn workspace_root(&self) -> &PathBuf;
1116}
1117
1118impl HasWorkspaceRoot for InteractiveCtx {
1119 fn workspace_root(&self) -> &PathBuf {
1120 &self.workspace_root
1121 }
1122}
1123
1124pub struct WorkspaceSandbox;
1129
1130#[async_trait]
1131impl<Ctx: HasWorkspaceRoot> SandboxProvider<Ctx> for WorkspaceSandbox {
1132 fn sandbox_enabled(_ctx: &Ctx) -> bool {
1133 true
1134 }
1135
1136 fn workspace_root(ctx: &Ctx) -> Option<&PathBuf> {
1137 Some(HasWorkspaceRoot::workspace_root(ctx))
1138 }
1139}
1140
1141delegate_components!(InteractiveCtx {
1142 ApprovalComponent => PromptApproval,
1143 SandboxComponent => WorkspaceSandbox,
1144 ExecuteComponent => PassthroughExecutor,
1145 MetadataComponent => PassthroughMetadata,
1146 LoggingComponent => TracingLogging,
1147 CacheComponent => NoCache,
1148 RetryComponent => NoRetry,
1149});
1150
1151pub struct CiCtx {
1153 pub workspace_root: PathBuf,
1154}
1155
1156impl CiCtx {
1157 pub fn new(workspace_root: PathBuf) -> Self {
1158 Self { workspace_root }
1159 }
1160}
1161
1162impl HasWorkspaceRoot for CiCtx {
1163 fn workspace_root(&self) -> &PathBuf {
1164 &self.workspace_root
1165 }
1166}
1167
1168pub type StrictWorkspaceSandbox = WorkspaceSandbox;
1170
1171delegate_components!(CiCtx {
1172 ApprovalComponent => AutoApproval,
1173 SandboxComponent => StrictWorkspaceSandbox,
1174 ExecuteComponent => PassthroughExecutor,
1175 MetadataComponent => PassthroughMetadata,
1176 LoggingComponent => NoLogging,
1177 CacheComponent => NoCache,
1178 RetryComponent => NoRetry,
1179});
1180
1181pub struct BenchCtx;
1183
1184delegate_components!(BenchCtx {
1185 ApprovalComponent => AutoApproval,
1186 SandboxComponent => NoSandbox,
1187 ExecuteComponent => PassthroughExecutor,
1188 MetadataComponent => PassthroughMetadata,
1189 LoggingComponent => NoLogging,
1190 CacheComponent => NoCache,
1191 RetryComponent => NoRetry,
1192});
1193
1194pub struct PassthroughExecutor;
1200
1201pub trait HasToolRef: Send + Sync {
1204 fn tool(&self) -> &dyn Tool;
1205}
1206
1207pub trait HasInnerTool: Send + Sync {
1212 fn inner_tool(&self) -> &dyn Tool;
1213}
1214
1215impl<Ctx> HasToolRef for Ctx
1216where
1217 Ctx: HasInnerTool + Send + Sync,
1218{
1219 fn tool(&self) -> &dyn Tool {
1220 self.inner_tool()
1221 }
1222}
1223
1224#[async_trait]
1225impl<Ctx: HasToolRef> ExecuteProvider<Ctx> for PassthroughExecutor {
1226 async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
1227 let tool = ctx.tool();
1228 tool.validate_args(&args)?;
1229 tool.execute(args).await
1230 }
1231
1232 async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
1233 let tool = ctx.tool();
1234 tool.validate_args(&args)?;
1235 tool.execute_dual(args).await
1236 }
1237}
1238
1239pub fn wrap_tool_interactive(
1252 tool: Arc<dyn Tool>,
1253 workspace_root: PathBuf,
1254) -> ToolFacade<ToolBridgeCtx<InteractiveCtx>> {
1255 let ctx = ToolBridgeCtx {
1256 inner: tool,
1257 runtime: InteractiveCtx::new(workspace_root),
1258 };
1259 ToolFacade::new(ctx)
1260}
1261
1262pub fn wrap_tool_ci(tool: Arc<dyn Tool>, workspace_root: PathBuf) -> ToolFacade<ToolBridgeCtx<CiCtx>> {
1268 let ctx = ToolBridgeCtx { inner: tool, runtime: CiCtx::new(workspace_root) };
1269 ToolFacade::new(ctx)
1270}
1271
1272pub struct ToolBridgeCtx<Runtime> {
1279 inner: Arc<dyn Tool>,
1280 runtime: Runtime,
1284}
1285
1286impl<Runtime: HasWorkspaceRoot> HasWorkspaceRoot for ToolBridgeCtx<Runtime> {
1287 fn workspace_root(&self) -> &PathBuf {
1288 self.runtime.workspace_root()
1289 }
1290}
1291
1292impl<Runtime: Send + Sync> HasInnerTool for ToolBridgeCtx<Runtime> {
1293 fn inner_tool(&self) -> &dyn Tool {
1294 self.inner.as_ref()
1295 }
1296}
1297
1298impl<Name, Runtime> HasComponent<Name> for ToolBridgeCtx<Runtime>
1300where
1301 Runtime: HasComponent<Name>,
1302{
1303 type Provider = ComponentProvider<Runtime, Name>;
1304}
1305
1306pub trait HasToolInstance<T>: Send + Sync {
1308 fn tool_instance(&self) -> &T;
1309}
1310
1311pub struct TypedToolExecutor<T>(PhantomData<T>);
1313
1314#[async_trait]
1315impl<Ctx, T> ExecuteProvider<Ctx> for TypedToolExecutor<T>
1316where
1317 Ctx: HasToolRef + Send + Sync,
1318 T: Tool + Send + Sync,
1319{
1320 async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
1321 <PassthroughExecutor as ExecuteProvider<Ctx>>::execute(ctx, args).await
1322 }
1323
1324 async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
1325 <PassthroughExecutor as ExecuteProvider<Ctx>>::execute_dual(ctx, args).await
1326 }
1327}
1328
1329pub struct TypedToolMetadata<T>(PhantomData<T>);
1331
1332impl<Ctx, T> MetadataProvider<Ctx> for TypedToolMetadata<T>
1333where
1334 Ctx: HasToolRef,
1335 T: Tool + Send + Sync,
1336{
1337 fn tool_name(ctx: &Ctx) -> &str {
1338 <PassthroughMetadata as MetadataProvider<Ctx>>::tool_name(ctx)
1339 }
1340
1341 fn tool_description(ctx: &Ctx) -> &str {
1342 <PassthroughMetadata as MetadataProvider<Ctx>>::tool_description(ctx)
1343 }
1344
1345 fn parameter_schema(ctx: &Ctx) -> Option<Value> {
1346 <PassthroughMetadata as MetadataProvider<Ctx>>::parameter_schema(ctx)
1347 }
1348
1349 fn config_schema(ctx: &Ctx) -> Option<Value> {
1350 <PassthroughMetadata as MetadataProvider<Ctx>>::config_schema(ctx)
1351 }
1352
1353 fn state_schema(ctx: &Ctx) -> Option<Value> {
1354 <PassthroughMetadata as MetadataProvider<Ctx>>::state_schema(ctx)
1355 }
1356
1357 fn prompt_path(ctx: &Ctx) -> Option<Cow<'static, str>> {
1358 <PassthroughMetadata as MetadataProvider<Ctx>>::prompt_path(ctx)
1359 }
1360
1361 fn default_permission(ctx: &Ctx) -> ToolPolicy {
1362 <PassthroughMetadata as MetadataProvider<Ctx>>::default_permission(ctx)
1363 }
1364
1365 fn allow_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
1366 <PassthroughMetadata as MetadataProvider<Ctx>>::allow_patterns(ctx)
1367 }
1368
1369 fn deny_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
1370 <PassthroughMetadata as MetadataProvider<Ctx>>::deny_patterns(ctx)
1371 }
1372
1373 fn is_mutating(ctx: &Ctx) -> bool {
1374 <PassthroughMetadata as MetadataProvider<Ctx>>::is_mutating(ctx)
1375 }
1376
1377 fn is_parallel_safe(ctx: &Ctx) -> bool {
1378 <PassthroughMetadata as MetadataProvider<Ctx>>::is_parallel_safe(ctx)
1379 }
1380
1381 fn tool_kind(ctx: &Ctx) -> &'static str {
1382 <PassthroughMetadata as MetadataProvider<Ctx>>::tool_kind(ctx)
1383 }
1384
1385 fn resource_hints(ctx: &Ctx, args: &Value) -> Vec<String> {
1386 <PassthroughMetadata as MetadataProvider<Ctx>>::resource_hints(ctx, args)
1387 }
1388
1389 fn execution_cost(ctx: &Ctx) -> u8 {
1390 <PassthroughMetadata as MetadataProvider<Ctx>>::execution_cost(ctx)
1391 }
1392}
1393
1394pub struct TypedToolCtx<Runtime, T> {
1396 tool: T,
1397 runtime: Runtime,
1398}
1399
1400impl<Runtime, T> TypedToolCtx<Runtime, T> {
1401 pub fn new(tool: T, runtime: Runtime) -> Self {
1402 Self { tool, runtime }
1403 }
1404}
1405
1406impl<Runtime: HasWorkspaceRoot + Send + Sync, T: Send + Sync> HasWorkspaceRoot for TypedToolCtx<Runtime, T> {
1407 fn workspace_root(&self) -> &PathBuf {
1408 self.runtime.workspace_root()
1409 }
1410}
1411
1412impl<Runtime: Send + Sync, T: Send + Sync> HasToolInstance<T> for TypedToolCtx<Runtime, T> {
1413 fn tool_instance(&self) -> &T {
1414 &self.tool
1415 }
1416}
1417
1418impl<Runtime: Send + Sync, T> HasToolRef for TypedToolCtx<Runtime, T>
1419where
1420 T: Tool + Send + Sync,
1421{
1422 fn tool(&self) -> &dyn Tool {
1423 &self.tool
1424 }
1425}
1426
1427macro_rules! delegate_runtime_components_for_typed_ctx {
1428 ($($component:ty),+ $(,)?) => {
1429 $(
1430 impl<Runtime, T> HasComponent<$component> for TypedToolCtx<Runtime, T>
1431 where
1432 Runtime: HasComponent<$component>,
1433 {
1434 type Provider = ComponentProvider<Runtime, $component>;
1435 }
1436 )+
1437 };
1438}
1439
1440delegate_runtime_components_for_typed_ctx!(
1441 ApprovalComponent,
1442 SandboxComponent,
1443 SessionComponent,
1444 OutputMapComponent,
1445 LoggingComponent,
1446 CacheComponent,
1447 RetryComponent,
1448);
1449
1450impl<Runtime, T> HasComponent<ExecuteComponent> for TypedToolCtx<Runtime, T>
1451where
1452 Runtime: Send + Sync,
1453 T: Tool + Send + Sync,
1454{
1455 type Provider = TypedToolExecutor<T>;
1456}
1457
1458impl<Runtime, T> HasComponent<MetadataComponent> for TypedToolCtx<Runtime, T>
1459where
1460 Runtime: Send + Sync,
1461 T: Tool + Send + Sync,
1462{
1463 type Provider = TypedToolMetadata<T>;
1464}
1465
1466pub fn wrap_native_tool_interactive<T>(tool: T, workspace_root: PathBuf) -> ToolFacade<TypedToolCtx<InteractiveCtx, T>>
1472where
1473 T: Tool + Send + Sync + 'static,
1474{
1475 ToolFacade::new(TypedToolCtx::new(tool, InteractiveCtx::new(workspace_root)))
1476}
1477
1478pub fn wrap_native_tool_ci<T>(tool: T, workspace_root: PathBuf) -> ToolFacade<TypedToolCtx<CiCtx, T>>
1484where
1485 T: Tool + Send + Sync + 'static,
1486{
1487 ToolFacade::new(TypedToolCtx::new(tool, CiCtx::new(workspace_root)))
1488}
1489
1490#[cfg(test)]
1491mod tests;