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