1use crate::command::{
22 CommandDescriptor, CommandExecutionContext, CommandResult, ExecuteCommandRequest,
23};
24use crate::deployment::DeploymentGrade;
25use crate::events::TokenUsage;
26use crate::mcp_server::{ScopedMcpServers, merge_scoped_mcp_servers};
27use crate::message::Message;
28use crate::message_filter::MessageFilterProvider;
29use crate::runtime_agent::RuntimeAgent;
30use crate::tool_types::{ToolCall, ToolDefinition};
31use crate::tools::{Tool, ToolRegistry};
32use crate::traits::SessionFileSystem;
33use crate::typed_id::SessionId;
34use async_trait::async_trait;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::sync::Arc;
38
39pub struct IntegrationPlugin {
63 pub experimental_only: bool,
65 pub feature_flag: Option<&'static str>,
68 pub factory: fn() -> Box<dyn Capability>,
70}
71
72inventory::collect!(IntegrationPlugin);
73
74pub use crate::capability_types::{
76 AgentCapabilityConfig, CapabilityId, CapabilityStatus, MountAccess, MountDirectoryBuilder,
77 MountEntry, MountPoint, MountSource,
78};
79
80mod a2a_delegation;
85#[cfg(feature = "ui-capabilities")]
86mod a2ui;
87mod agent_handoff;
88mod agent_instructions;
89pub mod attach_skill;
90mod background_execution;
91mod btw;
92mod budgeting;
93pub mod compaction;
94mod current_time;
95mod data_knowledge;
96mod declarative;
97mod fake_aws;
98mod fake_crm;
99mod fake_financial;
100mod fake_warehouse;
101mod file_system;
102mod human_intent;
103mod infinity_context;
104mod knowledge_base;
105mod loop_detection;
106pub mod mcp;
107mod noop;
108mod openai_tool_search;
109#[cfg(feature = "ui-capabilities")]
110mod openui;
111mod parallel;
112pub mod persistent_memory;
113mod platform_management;
114mod prompt_caching;
115mod prompt_canary_guardrail;
116mod research;
117mod sample_data;
118mod self_budget;
119mod session;
120mod session_sandbox;
121mod session_schedule;
122mod session_sql_database;
123mod session_storage;
124mod skills;
125mod stateless_todo_list;
126mod subagents;
127mod system_commands;
128mod test_math;
129mod test_weather;
130mod tool_output_persistence;
131pub mod user_hooks;
132mod virtual_bash;
133mod web_fetch;
134mod workspace_volumes;
135
136pub use a2a_delegation::{
138 A2A_AGENT_DELEGATION_CAPABILITY_ID, A2aAgentDelegationCapability, CancelAgentTool,
139 GetAgentRunsTool, MessageAgentTool, SpawnAgentTool, WaitAgentTool,
140};
141#[cfg(feature = "ui-capabilities")]
142pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
143pub use agent_handoff::{
144 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
145 MessageAgentHandoffTool, StartAgentHandoffTool,
146};
147pub use agent_instructions::{
148 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
149 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
150 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
151};
152pub use attach_skill::{
153 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
154 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
155 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
156};
157pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
158pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
159pub use budgeting::BudgetingCapability;
160pub use compaction::{
161 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
162 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
163 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
164 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
165 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
166 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
167 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
168};
169pub use current_time::{CurrentTimeCapability, GetCurrentTimeTool};
170pub use data_knowledge::DataKnowledgeCapability;
171pub use declarative::{
172 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
173 DeclarativeCapabilitySkill, declarative_capability_id, declarative_capability_info,
174 hydrate_declarative_capability_config, is_declarative_capability,
175 parse_declarative_capability_id, validate_declarative_capability_definition,
176};
177pub use fake_aws::{
178 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
179 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
180 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
181 AwsStopEc2InstanceTool, FakeAwsCapability,
182};
183pub use fake_crm::{
184 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
185 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
186 FakeCrmCapability,
187};
188pub use fake_financial::{
189 FakeFinancialCapability, FinanceCreateBudgetTool, FinanceCreateTransactionTool,
190 FinanceForecastCashFlowTool, FinanceGetBalanceTool, FinanceGetExpenseReportTool,
191 FinanceGetRevenueReportTool, FinanceListBudgetsTool, FinanceListTransactionsTool,
192};
193pub use fake_warehouse::{
194 FakeWarehouseCapability, WarehouseCreateInvoiceTool, WarehouseCreateOrderTool,
195 WarehouseCreateShipmentTool, WarehouseGetInventoryTool, WarehouseInventoryReportTool,
196 WarehouseListOrdersTool, WarehouseListShipmentsTool, WarehouseProcessReturnTool,
197 WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
198};
199pub use file_system::{
200 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
201 ReadFileTool, StatFileTool, WriteFileTool,
202};
203pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
204pub use infinity_context::{
205 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
206};
207pub use knowledge_base::{
208 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
209 validate_knowledge_base_config,
210};
211pub use loop_detection::LoopDetectionCapability;
212pub use mcp::{
213 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
214 parse_mcp_capability_id,
215};
216pub use noop::NoopCapability;
217pub use openai_tool_search::{
218 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
219};
220#[cfg(feature = "ui-capabilities")]
221pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
222pub use parallel::ParallelCapability;
223pub use persistent_memory::{
224 ForgetTool, MEMORY_CAPABILITY_ID, MemoryCapability, MemoryConfig, RecallTool, RememberTool,
225};
226pub use platform_management::{
227 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PlatformManagementCapability,
228 ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool, ReadSessionsTool,
229 SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
230};
231pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
232pub use prompt_canary_guardrail::{
233 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
234 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
235 REASON_CODE_SYSTEM_PROMPT_LEAK,
236};
237pub use research::ResearchCapability;
238pub use sample_data::SampleDataCapability;
239pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
240pub use session::{GetSessionInfoTool, SessionCapability, WriteSessionTitleTool};
241pub use session_sandbox::{
242 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
243 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
244};
245pub use session_schedule::{
246 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
247 SessionScheduleCapability,
248};
249pub use session_sql_database::{
250 SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool, SqlSchemaTool,
251};
252pub use session_storage::{KvStoreTool, SecretStoreTool, SessionStorageCapability};
253pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
254pub use stateless_todo_list::{StatelessTodoListCapability, WriteTodosTool};
255pub use subagents::SubagentCapability;
256pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
258pub use test_math::{AddTool, DivideTool, MultiplyTool, SubtractTool, TestMathCapability};
259pub use test_weather::{GetForecastTool, GetWeatherTool, TestWeatherCapability};
260pub use tool_output_persistence::{PersistOutputHook, ToolOutputPersistenceCapability};
261pub use user_hooks::UserHooksCapability;
262pub use virtual_bash::{BashTool, SessionFileSystemAdapter, VirtualBashCapability};
263pub use web_fetch::{
264 BotAuthPublicKey, WebFetchCapability, WebFetchTool, derive_bot_auth_public_key,
265};
266pub use workspace_volumes::{WORKSPACE_VOLUMES_CAPABILITY_ID, WorkspaceVolumesCapability};
267
268pub struct SystemPromptContext {
278 pub session_id: SessionId,
280 pub locale: Option<String>,
282 pub file_store: Option<Arc<dyn SessionFileSystem>>,
284}
285
286impl SystemPromptContext {
287 pub fn without_file_store(session_id: SessionId) -> Self {
289 Self {
290 session_id,
291 locale: None,
292 file_store: None,
293 }
294 }
295}
296
297#[async_trait]
344pub trait Capability: Send + Sync {
345 fn id(&self) -> &str;
347
348 fn name(&self) -> &str;
350
351 fn description(&self) -> &str;
353
354 fn status(&self) -> CapabilityStatus {
356 CapabilityStatus::Available
357 }
358
359 fn icon(&self) -> Option<&str> {
361 None
362 }
363
364 fn category(&self) -> Option<&str> {
366 None
367 }
368
369 fn system_prompt_addition(&self) -> Option<&str> {
389 None
390 }
391
392 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
404 self.system_prompt_addition().map(|addition| {
405 format!(
406 "<capability id=\"{}\">\n{}\n</capability>",
407 self.id(),
408 addition
409 )
410 })
411 }
412
413 fn system_prompt_preview(&self) -> Option<String> {
419 self.system_prompt_addition().map(|s| s.to_string())
420 }
421
422 fn tools(&self) -> Vec<Box<dyn Tool>> {
424 vec![]
425 }
426
427 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
435 self.tools()
436 }
437
438 async fn system_prompt_contribution_with_config(
445 &self,
446 ctx: &SystemPromptContext,
447 _config: &serde_json::Value,
448 ) -> Option<String> {
449 self.system_prompt_contribution(ctx).await
450 }
451
452 fn tool_definitions(&self) -> Vec<ToolDefinition> {
455 self.tools().iter().map(|t| t.to_definition()).collect()
456 }
457
458 fn mounts(&self) -> Vec<MountPoint> {
466 vec![]
467 }
468
469 fn dependencies(&self) -> Vec<&'static str> {
478 vec![]
479 }
480
481 fn features(&self) -> Vec<&'static str> {
496 vec![]
497 }
498
499 fn config_schema(&self) -> Option<serde_json::Value> {
505 None
506 }
507
508 fn config_ui_schema(&self) -> Option<serde_json::Value> {
513 None
514 }
515
516 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
522 Ok(())
523 }
524
525 fn mcp_servers(&self) -> ScopedMcpServers {
531 ScopedMcpServers::default()
532 }
533
534 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
536 self.mcp_servers()
537 }
538
539 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
552 None
553 }
554
555 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
563 None
564 }
565
566 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
574 vec![]
575 }
576
577 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
586 vec![]
587 }
588
589 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
597 vec![]
598 }
599
600 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
616 vec![]
617 }
618
619 fn user_hooks_with_config(
625 &self,
626 _config: &serde_json::Value,
627 ) -> Vec<crate::user_hook_types::UserHookSpec> {
628 self.user_hooks()
629 }
630
631 fn risk_level(&self) -> RiskLevel {
639 RiskLevel::Low
640 }
641
642 fn commands(&self) -> Vec<CommandDescriptor> {
650 vec![]
651 }
652
653 async fn execute_command(
667 &self,
668 request: &ExecuteCommandRequest,
669 _ctx: &CommandExecutionContext,
670 ) -> crate::error::Result<CommandResult> {
671 Err(crate::error::AgentLoopError::config(format!(
672 "capability {} declared command /{} but does not implement execute_command",
673 self.id(),
674 request.name,
675 )))
676 }
677
678 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
686 vec![]
687 }
688
689 fn contribute_skills(&self) -> Vec<SkillContribution> {
699 vec![]
700 }
701
702 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
713 vec![]
714 }
715}
716
717pub trait ToolDefinitionHook: Send + Sync {
718 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
719}
720
721pub trait ToolCallHook: Send + Sync {
722 fn narration(
723 &self,
724 _tool_def: Option<&ToolDefinition>,
725 _tool_call: &ToolCall,
726 _phase: crate::tool_narration::ToolNarrationPhase,
727 _locale: Option<&str>,
728 ) -> Option<String> {
729 None
730 }
731
732 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
733 tool_call
734 }
735}
736
737#[derive(
741 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
742)]
743#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
744#[cfg_attr(feature = "openapi", schema(example = "low"))]
745#[serde(rename_all = "lowercase")]
746pub enum RiskLevel {
747 Low,
749 Medium,
751 High,
753}
754
755#[derive(Debug, Clone, Serialize, Deserialize)]
761#[serde(rename_all = "snake_case")]
762pub enum BlueprintModel {
763 Fixed(String),
765 Default(String),
767 Inherit,
769}
770
771pub struct AgentBlueprint {
777 pub id: &'static str,
779 pub name: &'static str,
781 pub description: &'static str,
783 pub model: BlueprintModel,
785 pub system_prompt: &'static str,
787 pub tools: Vec<Box<dyn Tool>>,
789 pub max_turns: Option<usize>,
791 pub config_schema: Option<serde_json::Value>,
793}
794
795impl AgentBlueprint {
796 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
798 self.tools.iter().map(|t| t.to_definition()).collect()
799 }
800}
801
802impl std::fmt::Debug for AgentBlueprint {
803 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804 f.debug_struct("AgentBlueprint")
805 .field("id", &self.id)
806 .field("name", &self.name)
807 .field("model", &self.model)
808 .field("tool_count", &self.tools.len())
809 .field("max_turns", &self.max_turns)
810 .finish()
811 }
812}
813
814#[derive(Clone)]
841pub struct CapabilityRegistry {
842 capabilities: HashMap<String, Arc<dyn Capability>>,
843}
844
845impl CapabilityRegistry {
846 pub fn new() -> Self {
848 Self {
849 capabilities: HashMap::new(),
850 }
851 }
852
853 pub fn with_builtins() -> Self {
858 Self::with_builtins_for_grade(DeploymentGrade::from_env())
859 }
860
861 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
866 let mut registry = Self::new();
867
868 registry.register(AgentInstructionsCapability);
870 registry.register(HumanIntentCapability);
871 registry.register(NoopCapability);
872 registry.register(CurrentTimeCapability);
873 registry.register(ResearchCapability);
874 registry.register(PlatformManagementCapability);
875 registry.register(FileSystemCapability);
876 registry.register(WorkspaceVolumesCapability);
877 registry.register(SessionStorageCapability);
878 registry.register(SessionCapability);
879 registry.register(SessionSqlDatabaseCapability);
880 registry.register(TestMathCapability);
881 registry.register(TestWeatherCapability);
882 registry.register(StatelessTodoListCapability);
883 registry.register(WebFetchCapability::from_env());
884 registry.register(VirtualBashCapability);
885 registry.register(BackgroundExecutionCapability);
886 registry.register(SessionScheduleCapability);
887 registry.register(BtwCapability);
888 registry.register(InfinityContextCapability);
889 registry.register(budgeting::BudgetingCapability);
890 registry.register(SelfBudgetCapability);
891 registry.register(ParallelCapability);
892 registry.register(CompactionCapability);
893 registry.register(MemoryCapability);
894
895 registry.register(OpenAiToolSearchCapability::new());
897 registry.register(PromptCachingCapability::new());
898
899 registry.register(SkillsCapability);
901
902 registry.register(SubagentCapability);
904
905 if crate::FeatureFlags::from_env(&grade).agent_delegation {
909 registry.register(AgentHandoffCapability);
910 registry.register(A2aAgentDelegationCapability);
911 }
912
913 registry.register(SystemCommandsCapability);
915
916 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
918
919 registry.register(user_hooks::UserHooksCapability);
922
923 registry.register(LoopDetectionCapability);
925
926 registry.register(PromptCanaryGuardrailCapability);
929
930 #[cfg(feature = "ui-capabilities")]
932 {
933 registry.register(OpenUiCapability);
934 registry.register(A2UiCapability);
935 }
936
937 registry.register(SampleDataCapability);
939
940 registry.register(DataKnowledgeCapability);
942
943 registry.register(KnowledgeBaseCapability);
945
946 registry.register(FakeWarehouseCapability);
948 registry.register(FakeAwsCapability);
949 registry.register(FakeCrmCapability);
950 registry.register(FakeFinancialCapability);
951
952 let internal_flags = crate::InternalFeatureFlags::from_env();
954 if internal_flags.session_sandbox {
955 registry.register(SessionSandboxCapability);
956 }
957 for plugin in inventory::iter::<IntegrationPlugin>() {
958 if (!plugin.experimental_only || grade.experimental_features_enabled())
959 && plugin
960 .feature_flag
961 .is_none_or(|f| internal_flags.is_enabled(f))
962 {
963 registry.register_boxed((plugin.factory)());
964 }
965 }
966
967 registry
968 }
969
970 pub fn register(&mut self, capability: impl Capability + 'static) {
972 self.capabilities
973 .insert(capability.id().to_string(), Arc::new(capability));
974 }
975
976 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
978 self.capabilities
979 .insert(capability.id().to_string(), Arc::from(capability));
980 }
981
982 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
984 self.capabilities
985 .insert(capability.id().to_string(), capability);
986 }
987
988 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
990 self.capabilities.get(id)
991 }
992
993 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
995 self.capabilities.remove(id)
996 }
997
998 pub fn has(&self, id: &str) -> bool {
1000 self.capabilities.contains_key(id)
1001 }
1002
1003 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1005 self.capabilities.values().collect()
1006 }
1007
1008 pub fn len(&self) -> usize {
1010 self.capabilities.len()
1011 }
1012
1013 pub fn is_empty(&self) -> bool {
1015 self.capabilities.is_empty()
1016 }
1017
1018 pub fn builder() -> CapabilityRegistryBuilder {
1020 CapabilityRegistryBuilder::new()
1021 }
1022
1023 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1027 for cap in self.capabilities.values() {
1028 for bp in cap.agent_blueprints() {
1029 if bp.id == id {
1030 return Some(bp);
1031 }
1032 }
1033 }
1034 None
1035 }
1036
1037 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1041 for (capability_id, cap) in &self.capabilities {
1042 for bp in cap.agent_blueprints() {
1043 if bp.id == id {
1044 return Some((capability_id.clone(), bp));
1045 }
1046 }
1047 }
1048 None
1049 }
1050
1051 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1053 self.capabilities
1054 .values()
1055 .flat_map(|cap| cap.agent_blueprints())
1056 .collect()
1057 }
1058}
1059
1060impl Default for CapabilityRegistry {
1061 fn default() -> Self {
1062 Self::with_builtins()
1063 }
1064}
1065
1066impl std::fmt::Debug for CapabilityRegistry {
1067 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068 let ids: Vec<_> = self.capabilities.keys().collect();
1069 f.debug_struct("CapabilityRegistry")
1070 .field("capabilities", &ids)
1071 .finish()
1072 }
1073}
1074
1075pub struct CapabilityRegistryBuilder {
1077 registry: CapabilityRegistry,
1078}
1079
1080impl CapabilityRegistryBuilder {
1081 pub fn new() -> Self {
1083 Self {
1084 registry: CapabilityRegistry::new(),
1085 }
1086 }
1087
1088 pub fn with_builtins() -> Self {
1090 Self {
1091 registry: CapabilityRegistry::with_builtins(),
1092 }
1093 }
1094
1095 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1097 self.registry.register(capability);
1098 self
1099 }
1100
1101 pub fn build(self) -> CapabilityRegistry {
1103 self.registry
1104 }
1105}
1106
1107impl Default for CapabilityRegistryBuilder {
1108 fn default() -> Self {
1109 Self::new()
1110 }
1111}
1112
1113pub struct ModelViewContext<'a> {
1119 pub session_id: SessionId,
1120 pub prior_usage: Option<&'a TokenUsage>,
1121}
1122
1123pub trait ModelViewProvider: Send + Sync {
1129 fn apply_model_view(
1130 &self,
1131 messages: Vec<Message>,
1132 config: &serde_json::Value,
1133 context: &ModelViewContext<'_>,
1134 ) -> Vec<Message>;
1135
1136 fn priority(&self) -> i32 {
1137 0
1138 }
1139}
1140
1141pub struct CollectedCapabilities {
1146 pub system_prompt_parts: Vec<String>,
1148 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1150 pub tools: Vec<Box<dyn Tool>>,
1152 pub tool_definitions: Vec<ToolDefinition>,
1154 pub mounts: Vec<MountPoint>,
1156 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1158 pub applied_ids: Vec<String>,
1160 pub tool_search: Option<crate::llm_driver_registry::ToolSearchConfig>,
1162 pub prompt_cache: Option<crate::llm_driver_registry::PromptCacheConfig>,
1164 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1166 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1168 pub mcp_servers: ScopedMcpServers,
1170 }
1176
1177#[derive(Debug, Clone, PartialEq, Eq)]
1178pub struct SystemPromptAttribution {
1179 pub capability_id: String,
1180 pub content: String,
1181}
1182
1183impl CollectedCapabilities {
1184 pub fn system_prompt_prefix(&self) -> Option<String> {
1187 if self.system_prompt_parts.is_empty() {
1188 None
1189 } else {
1190 Some(self.system_prompt_parts.join("\n\n"))
1191 }
1192 }
1193
1194 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1198 for (provider, config) in &self.message_filter_providers {
1200 provider.apply_filters(query, config);
1201 }
1202 }
1203
1204 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1207 for (provider, config) in &self.message_filter_providers {
1208 provider.post_load(messages, config);
1209 }
1210 }
1211
1212 pub fn has_message_filters(&self) -> bool {
1214 !self.message_filter_providers.is_empty()
1215 }
1216}
1217
1218pub struct CollectedMessageFilters {
1225 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1227}
1228
1229pub struct CollectedModelViewProviders {
1231 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1233}
1234
1235impl CollectedMessageFilters {
1241 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1243 for (provider, config) in &self.message_filter_providers {
1244 provider.apply_filters(query, config);
1245 }
1246 }
1247
1248 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1250 for (provider, config) in &self.message_filter_providers {
1251 provider.post_load(messages, config);
1252 }
1253 }
1254}
1255
1256impl CollectedModelViewProviders {
1257 pub fn apply_model_view(
1259 &self,
1260 mut messages: Vec<Message>,
1261 context: &ModelViewContext<'_>,
1262 ) -> Vec<Message> {
1263 for (provider, config) in &self.model_view_providers {
1264 messages = provider.apply_model_view(messages, config, context);
1265 }
1266 messages
1267 }
1268}
1269
1270pub fn collect_message_filters_only(
1276 capability_configs: &[AgentCapabilityConfig],
1277 registry: &CapabilityRegistry,
1278) -> CollectedMessageFilters {
1279 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1280 Vec::new();
1281
1282 for cap_config in capability_configs {
1283 let cap_id = cap_config.capability_ref.as_str();
1284 if let Some(capability) = registry.get(cap_id) {
1285 if capability.status() != CapabilityStatus::Available {
1286 continue;
1287 }
1288 if let Some(provider) = capability.message_filter_provider() {
1289 message_filter_providers.push((provider, cap_config.config.clone()));
1290 }
1291 }
1292 }
1293
1294 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1295
1296 CollectedMessageFilters {
1297 message_filter_providers,
1298 }
1299}
1300
1301pub fn collect_model_view_providers(
1303 capability_configs: &[AgentCapabilityConfig],
1304 registry: &CapabilityRegistry,
1305) -> CollectedModelViewProviders {
1306 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1307
1308 for cap_config in capability_configs {
1309 let cap_id = cap_config.capability_ref.as_str();
1310 if let Some(capability) = registry.get(cap_id) {
1311 if capability.status() != CapabilityStatus::Available {
1312 continue;
1313 }
1314 if let Some(provider) = capability.model_view_provider() {
1315 model_view_providers.push((provider, cap_config.config.clone()));
1316 }
1317 }
1318 }
1319
1320 model_view_providers.sort_by_key(|(p, _)| p.priority());
1321
1322 CollectedModelViewProviders {
1323 model_view_providers,
1324 }
1325}
1326
1327pub fn collect_capability_mcp_servers(
1328 capability_configs: &[AgentCapabilityConfig],
1329 registry: &CapabilityRegistry,
1330) -> ScopedMcpServers {
1331 let mut servers = ScopedMcpServers::default();
1332
1333 for cap_config in capability_configs {
1334 let cap_id = cap_config.capability_ref.as_str();
1335 if is_declarative_capability(cap_id) {
1336 if let Ok(definition) =
1337 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1338 {
1339 if definition.status != CapabilityStatus::Available {
1340 continue;
1341 }
1342 if let Some(contributed) = definition.mcp_servers {
1343 servers = merge_scoped_mcp_servers(&servers, &contributed);
1344 }
1345 }
1346 continue;
1347 }
1348 if let Some(capability) = registry.get(cap_id) {
1349 if capability.status() != CapabilityStatus::Available {
1350 continue;
1351 }
1352 servers = merge_scoped_mcp_servers(
1353 &servers,
1354 &capability.mcp_servers_with_config(&cap_config.config),
1355 );
1356 }
1357 }
1358
1359 servers
1360}
1361
1362pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1369
1370#[derive(Debug, Clone, PartialEq, Eq)]
1372pub enum DependencyError {
1373 CircularDependency {
1375 capability_id: String,
1377 chain: Vec<String>,
1379 },
1380 TooManyCapabilities {
1382 count: usize,
1384 max: usize,
1386 },
1387}
1388
1389impl std::fmt::Display for DependencyError {
1390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1391 match self {
1392 DependencyError::CircularDependency {
1393 capability_id,
1394 chain,
1395 } => {
1396 write!(
1397 f,
1398 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1399 capability_id,
1400 chain.join(" -> "),
1401 capability_id
1402 )
1403 }
1404 DependencyError::TooManyCapabilities { count, max } => {
1405 write!(
1406 f,
1407 "Too many capabilities after resolution: {} (max: {})",
1408 count, max
1409 )
1410 }
1411 }
1412 }
1413}
1414
1415impl std::error::Error for DependencyError {}
1416
1417#[derive(Debug, Clone)]
1419pub struct ResolvedCapabilities {
1420 pub resolved_ids: Vec<String>,
1423 pub added_as_dependencies: Vec<String>,
1425 pub user_selected: Vec<String>,
1427}
1428
1429pub fn resolve_dependencies(
1449 selected_ids: &[String],
1450 registry: &CapabilityRegistry,
1451) -> Result<ResolvedCapabilities, DependencyError> {
1452 use std::collections::HashSet;
1453
1454 let user_selected: HashSet<String> = selected_ids.iter().cloned().collect();
1455 let mut resolved: Vec<String> = Vec::new();
1456 let mut resolved_set: HashSet<String> = HashSet::new();
1457 let mut added_as_dependencies: Vec<String> = Vec::new();
1458
1459 for cap_id in selected_ids {
1461 resolve_single_capability(
1462 cap_id,
1463 registry,
1464 &mut resolved,
1465 &mut resolved_set,
1466 &mut added_as_dependencies,
1467 &user_selected,
1468 &mut Vec::new(), )?;
1470 }
1471
1472 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
1474 return Err(DependencyError::TooManyCapabilities {
1475 count: resolved.len(),
1476 max: MAX_RESOLVED_CAPABILITIES,
1477 });
1478 }
1479
1480 Ok(ResolvedCapabilities {
1481 resolved_ids: resolved,
1482 added_as_dependencies,
1483 user_selected: selected_ids.to_vec(),
1484 })
1485}
1486
1487pub fn resolve_capability_configs(
1492 selected_configs: &[AgentCapabilityConfig],
1493 registry: &CapabilityRegistry,
1494) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
1495 let mut selected_ids: Vec<String> = Vec::new();
1496 for config in selected_configs {
1497 if is_declarative_capability(config.capability_id())
1498 && let Ok(definition) =
1499 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
1500 {
1501 selected_ids.extend(definition.dependencies);
1502 }
1503 selected_ids.push(config.capability_id().to_string());
1504 }
1505 let resolved = resolve_dependencies(&selected_ids, registry)?;
1506
1507 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
1508 .iter()
1509 .map(|config| (config.capability_id().to_string(), config.config.clone()))
1510 .collect();
1511
1512 Ok(resolved
1513 .resolved_ids
1514 .into_iter()
1515 .map(|capability_id| {
1516 explicit_configs
1517 .get(&capability_id)
1518 .cloned()
1519 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
1520 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
1521 })
1522 .collect())
1523}
1524
1525fn resolve_single_capability(
1527 cap_id: &str,
1528 registry: &CapabilityRegistry,
1529 resolved: &mut Vec<String>,
1530 resolved_set: &mut std::collections::HashSet<String>,
1531 added_as_dependencies: &mut Vec<String>,
1532 user_selected: &std::collections::HashSet<String>,
1533 visiting: &mut Vec<String>,
1534) -> Result<(), DependencyError> {
1535 if resolved_set.contains(cap_id) {
1537 return Ok(());
1538 }
1539
1540 if visiting.contains(&cap_id.to_string()) {
1542 return Err(DependencyError::CircularDependency {
1543 capability_id: cap_id.to_string(),
1544 chain: visiting.clone(),
1545 });
1546 }
1547
1548 let capability = match registry.get(cap_id) {
1550 Some(cap) => cap,
1551 None => {
1552 if is_declarative_capability(cap_id) && !resolved_set.contains(cap_id) {
1553 resolved.push(cap_id.to_string());
1554 resolved_set.insert(cap_id.to_string());
1555 if !user_selected.contains(cap_id) {
1556 added_as_dependencies.push(cap_id.to_string());
1557 }
1558 }
1559 return Ok(());
1560 }
1561 };
1562
1563 visiting.push(cap_id.to_string());
1565
1566 for dep_id in capability.dependencies() {
1568 resolve_single_capability(
1569 dep_id,
1570 registry,
1571 resolved,
1572 resolved_set,
1573 added_as_dependencies,
1574 user_selected,
1575 visiting,
1576 )?;
1577 }
1578
1579 visiting.pop();
1581
1582 if !resolved_set.contains(cap_id) {
1584 resolved.push(cap_id.to_string());
1585 resolved_set.insert(cap_id.to_string());
1586
1587 if !user_selected.contains(cap_id) {
1589 added_as_dependencies.push(cap_id.to_string());
1590 }
1591 }
1592
1593 Ok(())
1594}
1595
1596pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
1601 use std::collections::HashSet;
1602
1603 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
1604 Ok(resolved) => resolved.resolved_ids,
1605 Err(_) => capability_ids.to_vec(),
1606 };
1607
1608 let mut seen = HashSet::new();
1609 let mut features = Vec::new();
1610 for cap_id in &resolved_ids {
1611 if let Some(cap) = registry.get(cap_id) {
1612 for feature in cap.features() {
1613 if seen.insert(feature) {
1614 features.push(feature.to_string());
1615 }
1616 }
1617 }
1618 }
1619 features
1620}
1621
1622pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
1625 registry
1626 .get(cap_id)
1627 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
1628 .unwrap_or_default()
1629}
1630
1631pub async fn collect_capabilities(
1647 capability_ids: &[String],
1648 registry: &CapabilityRegistry,
1649 ctx: &SystemPromptContext,
1650) -> CollectedCapabilities {
1651 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
1654 Ok(resolved) => resolved.resolved_ids,
1655 Err(e) => {
1656 tracing::warn!("Failed to resolve capability dependencies: {}", e);
1657 capability_ids.to_vec()
1658 }
1659 };
1660
1661 let configs: Vec<AgentCapabilityConfig> = resolved_ids
1663 .iter()
1664 .map(|id| AgentCapabilityConfig {
1665 capability_ref: CapabilityId::new(id),
1666 config: serde_json::Value::Object(serde_json::Map::new()),
1667 })
1668 .collect();
1669
1670 collect_capabilities_with_configs(&configs, registry, ctx).await
1671}
1672
1673pub async fn collect_capabilities_with_configs(
1684 capability_configs: &[AgentCapabilityConfig],
1685 registry: &CapabilityRegistry,
1686 ctx: &SystemPromptContext,
1687) -> CollectedCapabilities {
1688 let mut system_prompt_parts: Vec<String> = Vec::new();
1689 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
1690 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
1691 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
1692 let mut mounts: Vec<MountPoint> = Vec::new();
1693 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1694 Vec::new();
1695 let mut applied_ids: Vec<String> = Vec::new();
1696 let mut tool_search: Option<crate::llm_driver_registry::ToolSearchConfig> = None;
1697 let mut prompt_cache: Option<crate::llm_driver_registry::PromptCacheConfig> = None;
1698 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
1699 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
1700 let mut mcp_servers = ScopedMcpServers::default();
1701
1702 for cap_config in capability_configs {
1703 let cap_id = cap_config.capability_ref.as_str();
1704 if is_declarative_capability(cap_id) {
1705 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
1706 cap_config.config.clone(),
1707 ) {
1708 Ok(definition) => {
1709 if definition.status != CapabilityStatus::Available {
1710 continue;
1711 }
1712
1713 if let Some(prompt) = definition.system_prompt.as_deref() {
1714 let contribution =
1715 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
1716 system_prompt_attributions.push(SystemPromptAttribution {
1717 capability_id: cap_id.to_string(),
1718 content: contribution.clone(),
1719 });
1720 system_prompt_parts.push(contribution);
1721 }
1722
1723 mounts.extend(definition.mounts(cap_id));
1724 if let Some(ref servers) = definition.mcp_servers {
1725 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
1726 }
1727 for skill in definition.skill_contributions() {
1728 mounts.push(skill.to_mount(cap_id));
1729 }
1730
1731 applied_ids.push(cap_id.to_string());
1732 }
1733 Err(error) => {
1734 tracing::warn!(
1735 capability_id = %cap_id,
1736 error = %error,
1737 "Skipping invalid declarative capability config"
1738 );
1739 }
1740 }
1741 continue;
1742 }
1743 if let Some(capability) = registry.get(cap_id) {
1744 if capability.status() != CapabilityStatus::Available {
1746 continue;
1747 }
1748
1749 if let Some(contribution) = capability
1751 .system_prompt_contribution_with_config(ctx, &cap_config.config)
1752 .await
1753 {
1754 system_prompt_attributions.push(SystemPromptAttribution {
1755 capability_id: cap_id.to_string(),
1756 content: contribution.clone(),
1757 });
1758 system_prompt_parts.push(contribution);
1759 }
1760
1761 tools.extend(capability.tools_with_config(&cap_config.config));
1763 tool_definition_hooks.extend(capability.tool_definition_hooks());
1764 tool_call_hooks.extend(capability.tool_call_hooks());
1765 let cap_category = capability.category();
1770 for def in capability.tool_definitions() {
1771 let def = match (def.category(), cap_category) {
1772 (None, Some(cat)) => def.with_category(cat),
1773 _ => def,
1774 }
1775 .with_capability_attribution(cap_id, Some(capability.name()));
1776 tool_definitions.push(def);
1777 }
1778
1779 if cap_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID {
1781 let threshold = cap_config
1783 .config
1784 .get("threshold")
1785 .and_then(|v| v.as_u64())
1786 .map(|v| v as usize)
1787 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
1788 tool_search = Some(crate::llm_driver_registry::ToolSearchConfig {
1789 enabled: true,
1790 threshold,
1791 });
1792 }
1793
1794 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
1795 let strategy = cap_config
1796 .config
1797 .get("strategy")
1798 .and_then(|v| v.as_str())
1799 .map(|value| match value {
1800 "auto" => crate::llm_driver_registry::PromptCacheStrategy::Auto,
1801 _ => crate::llm_driver_registry::PromptCacheStrategy::Auto,
1802 })
1803 .unwrap_or(crate::llm_driver_registry::PromptCacheStrategy::Auto);
1804 let gemini_cached_content = cap_config
1805 .config
1806 .get("gemini_cached_content")
1807 .and_then(|v| v.as_str())
1808 .map(str::to_string);
1809 prompt_cache = Some(crate::llm_driver_registry::PromptCacheConfig {
1810 enabled: true,
1811 strategy,
1812 gemini_cached_content,
1813 });
1814 }
1815
1816 mounts.extend(capability.mounts());
1818
1819 mcp_servers = merge_scoped_mcp_servers(
1820 &mcp_servers,
1821 &capability.mcp_servers_with_config(&cap_config.config),
1822 );
1823
1824 for skill in capability.contribute_skills() {
1828 mounts.push(skill.to_mount(cap_id));
1829 }
1830
1831 if let Some(provider) = capability.message_filter_provider() {
1833 message_filter_providers.push((provider, cap_config.config.clone()));
1834 }
1835
1836 applied_ids.push(cap_id.to_string());
1837 }
1838 }
1839
1840 if !applied_ids
1852 .iter()
1853 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
1854 && tool_definitions
1855 .iter()
1856 .any(|def| def.hints().supports_background == Some(true))
1857 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
1858 && bg_cap.status() == CapabilityStatus::Available
1859 {
1860 tools.extend(bg_cap.tools());
1861 let cap_category = bg_cap.category();
1862 for def in bg_cap.tool_definitions() {
1863 let def = match (def.category(), cap_category) {
1864 (None, Some(cat)) => def.with_category(cat),
1865 _ => def,
1866 }
1867 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
1868 tool_definitions.push(def);
1869 }
1870 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
1871 }
1872
1873 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1875
1876 CollectedCapabilities {
1877 system_prompt_parts,
1878 system_prompt_attributions,
1879 tools,
1880 tool_definitions,
1881 mounts,
1882 message_filter_providers,
1883 applied_ids,
1884 tool_search,
1885 prompt_cache,
1886 tool_definition_hooks,
1887 tool_call_hooks,
1888 mcp_servers,
1889 }
1890}
1891
1892pub struct AppliedCapabilities {
1898 pub runtime_agent: RuntimeAgent,
1900 pub tool_registry: ToolRegistry,
1902 pub applied_ids: Vec<String>,
1904}
1905
1906pub async fn apply_capabilities(
1943 base_runtime_agent: RuntimeAgent,
1944 capability_ids: &[String],
1945 registry: &CapabilityRegistry,
1946 ctx: &SystemPromptContext,
1947) -> AppliedCapabilities {
1948 let collected = collect_capabilities(capability_ids, registry, ctx).await;
1949
1950 let final_system_prompt = match collected.system_prompt_prefix() {
1952 Some(prefix) => format!(
1953 "{}\n\n<system-prompt>\n{}\n</system-prompt>",
1954 prefix, base_runtime_agent.system_prompt
1955 ),
1956 None => base_runtime_agent.system_prompt,
1957 };
1958
1959 let mut tool_registry = ToolRegistry::new();
1961 for tool in collected.tools {
1962 tool_registry.register_boxed(tool);
1963 }
1964
1965 let mut tools = collected.tool_definitions;
1967 for hook in &collected.tool_definition_hooks {
1968 tools = hook.transform(tools);
1969 }
1970
1971 let runtime_agent = RuntimeAgent {
1972 system_prompt: final_system_prompt,
1973 model: base_runtime_agent.model,
1974 tools,
1975 max_iterations: base_runtime_agent.max_iterations,
1976 temperature: base_runtime_agent.temperature,
1977 max_tokens: base_runtime_agent.max_tokens,
1978 tool_search: collected.tool_search,
1979 prompt_cache: collected.prompt_cache,
1980 network_access: base_runtime_agent.network_access,
1981 };
1982
1983 AppliedCapabilities {
1984 runtime_agent,
1985 tool_registry,
1986 applied_ids: collected.applied_ids,
1987 }
1988}
1989
1990#[cfg(test)]
1995mod tests {
1996 use super::*;
1997 use crate::typed_id::SessionId;
1998 use std::collections::BTreeSet;
1999 use uuid::Uuid;
2000
2001 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2003
2004 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2005 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2006 }
2007
2008 fn test_ctx() -> SystemPromptContext {
2010 SystemPromptContext::without_file_store(SessionId::new())
2011 }
2012
2013 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2015 let mut ids = [
2016 "agent_instructions",
2017 "human_intent",
2018 "budgeting",
2019 "self_budget",
2020 "parallel",
2021 "noop",
2022 "current_time",
2023 "research",
2024 "platform_management",
2025 "session_file_system",
2026 "workspace_volumes",
2027 "session_storage",
2028 "session",
2029 "session_sql_database",
2030 "test_math",
2031 "test_weather",
2032 "stateless_todo_list",
2033 "web_fetch",
2034 "virtual_bash",
2035 "background_execution",
2036 "session_schedule",
2037 "btw",
2038 "infinity_context",
2039 "compaction",
2040 "memory",
2041 "openai_tool_search",
2042 "prompt_caching",
2043 "skills",
2044 "subagents",
2045 "system_commands",
2046 "sample_data",
2047 "data_knowledge",
2048 "knowledge_base",
2049 "tool_output_persistence",
2050 "fake_warehouse",
2051 "fake_aws",
2052 "fake_crm",
2053 "fake_financial",
2054 "loop_detection",
2055 "prompt_canary_guardrail",
2056 "user_hooks",
2057 ]
2058 .into_iter()
2059 .collect::<BTreeSet<_>>();
2060 if cfg!(feature = "ui-capabilities") {
2061 ids.insert("openui");
2062 ids.insert("a2ui");
2063 }
2064 ids
2065 }
2066
2067 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2069 let mut ids = expected_core_builtin_ids();
2070 ids.insert("agent_handoff");
2071 ids.insert("a2a_agent_delegation");
2072 ids
2073 }
2074
2075 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2076 registry.capabilities.keys().map(String::as_str).collect()
2077 }
2078
2079 #[test]
2089 fn test_capability_registry_with_builtins_dev() {
2090 let _lock = lock_env();
2092 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2093 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2094 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2095 assert!(registry.has("agent_handoff"));
2096 assert!(registry.has("a2a_agent_delegation"));
2097 }
2098
2099 #[test]
2100 fn test_capability_registry_with_builtins_prod() {
2101 let _lock = lock_env();
2103 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2104 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2105 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2106 assert!(!registry.has("docker_container"));
2108 assert!(!registry.has("agent_handoff"));
2109 assert!(!registry.has("a2a_agent_delegation"));
2110 }
2111
2112 #[test]
2113 fn test_agent_delegation_enabled_by_env_in_prod() {
2114 let _lock = lock_env();
2116 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2117 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2118 assert!(registry.has("agent_handoff"));
2119 assert!(registry.has("a2a_agent_delegation"));
2120 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2121 }
2122
2123 #[test]
2124 fn test_agent_delegation_disabled_by_env_in_dev() {
2125 let _lock = lock_env();
2127 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2128 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2129 assert!(!registry.has("agent_handoff"));
2130 assert!(!registry.has("a2a_agent_delegation"));
2131 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2132 }
2133
2134 #[test]
2135 fn test_capability_registry_get() {
2136 let registry = CapabilityRegistry::with_builtins();
2137
2138 let noop = registry.get("noop").unwrap();
2139 assert_eq!(noop.id(), "noop");
2140 assert_eq!(noop.name(), "No-Op");
2141 assert_eq!(noop.status(), CapabilityStatus::Available);
2142 }
2143
2144 #[test]
2145 fn test_capability_registry_blueprint_with_capability() {
2146 struct BlueprintProviderCapability;
2147
2148 impl Capability for BlueprintProviderCapability {
2149 fn id(&self) -> &str {
2150 "blueprint_provider"
2151 }
2152 fn name(&self) -> &str {
2153 "Blueprint Provider"
2154 }
2155 fn description(&self) -> &str {
2156 "Capability that provides a blueprint for tests"
2157 }
2158 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2159 vec![AgentBlueprint {
2160 id: "test_blueprint",
2161 name: "Test Blueprint",
2162 description: "Blueprint for capability registry tests",
2163 model: BlueprintModel::Inherit,
2164 system_prompt: "Test prompt",
2165 tools: vec![],
2166 max_turns: None,
2167 config_schema: None,
2168 }]
2169 }
2170 }
2171
2172 let mut registry = CapabilityRegistry::new();
2173 registry.register(BlueprintProviderCapability);
2174
2175 let (capability_id, blueprint) = registry
2176 .blueprint_with_capability("test_blueprint")
2177 .expect("blueprint should resolve with capability id");
2178 assert_eq!(capability_id, "blueprint_provider");
2179 assert_eq!(blueprint.id, "test_blueprint");
2180 }
2181
2182 #[test]
2183 fn test_capability_registry_builder() {
2184 let registry = CapabilityRegistry::builder()
2185 .capability(NoopCapability)
2186 .capability(CurrentTimeCapability)
2187 .build();
2188
2189 assert!(registry.has("noop"));
2190 assert!(registry.has("current_time"));
2191 assert_eq!(registry.len(), 2);
2192 }
2193
2194 #[test]
2195 fn test_capability_status() {
2196 let registry = CapabilityRegistry::with_builtins();
2197
2198 let current_time = registry.get("current_time").unwrap();
2199 assert_eq!(current_time.status(), CapabilityStatus::Available);
2200
2201 let research = registry.get("research").unwrap();
2202 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2203 }
2204
2205 #[test]
2206 fn test_capability_icons_and_categories() {
2207 let registry = CapabilityRegistry::with_builtins();
2208
2209 let noop = registry.get("noop").unwrap();
2210 assert_eq!(noop.icon(), Some("circle-off"));
2211 assert_eq!(noop.category(), Some("Testing"));
2212
2213 let current_time = registry.get("current_time").unwrap();
2214 assert_eq!(current_time.icon(), Some("clock"));
2215 assert_eq!(current_time.category(), Some("Utilities"));
2216 }
2217
2218 #[test]
2219 fn test_system_prompt_preview_default_delegates_to_addition() {
2220 let registry = CapabilityRegistry::with_builtins();
2221
2222 let test_math = registry.get("test_math").unwrap();
2224 assert_eq!(
2225 test_math.system_prompt_preview().as_deref(),
2226 test_math.system_prompt_addition()
2227 );
2228
2229 let current_time = registry.get("current_time").unwrap();
2231 assert!(current_time.system_prompt_preview().is_none());
2232 assert!(current_time.system_prompt_addition().is_none());
2233 }
2234
2235 #[test]
2236 fn test_system_prompt_preview_dynamic_capability() {
2237 let registry = CapabilityRegistry::with_builtins();
2238 let cap = registry.get("agent_instructions").unwrap();
2239
2240 assert!(cap.system_prompt_addition().is_none());
2242 assert!(cap.system_prompt_preview().is_some());
2243 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2244 }
2245
2246 #[tokio::test]
2251 async fn test_apply_capabilities_empty() {
2252 let registry = CapabilityRegistry::with_builtins();
2253 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2254
2255 let applied =
2256 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
2257
2258 assert_eq!(
2259 applied.runtime_agent.system_prompt,
2260 base_runtime_agent.system_prompt
2261 );
2262 assert!(applied.tool_registry.is_empty());
2263 assert!(applied.applied_ids.is_empty());
2264 }
2265
2266 #[tokio::test]
2267 async fn test_apply_capabilities_noop() {
2268 let registry = CapabilityRegistry::with_builtins();
2269 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2270
2271 let applied = apply_capabilities(
2272 base_runtime_agent.clone(),
2273 &["noop".to_string()],
2274 ®istry,
2275 &test_ctx(),
2276 )
2277 .await;
2278
2279 assert_eq!(
2281 applied.runtime_agent.system_prompt,
2282 base_runtime_agent.system_prompt
2283 );
2284 assert!(applied.tool_registry.is_empty());
2285 assert_eq!(applied.applied_ids, vec!["noop"]);
2286 }
2287
2288 #[tokio::test]
2289 async fn test_apply_capabilities_current_time() {
2290 let registry = CapabilityRegistry::with_builtins();
2291 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2292
2293 let applied = apply_capabilities(
2294 base_runtime_agent.clone(),
2295 &["current_time".to_string()],
2296 ®istry,
2297 &test_ctx(),
2298 )
2299 .await;
2300
2301 assert_eq!(
2303 applied.runtime_agent.system_prompt,
2304 base_runtime_agent.system_prompt
2305 );
2306 assert!(applied.tool_registry.has("get_current_time"));
2307 assert_eq!(applied.tool_registry.len(), 1);
2308 assert_eq!(applied.applied_ids, vec!["current_time"]);
2309 }
2310
2311 #[tokio::test]
2312 async fn test_apply_capabilities_skips_coming_soon() {
2313 let registry = CapabilityRegistry::with_builtins();
2314 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2315
2316 let applied = apply_capabilities(
2318 base_runtime_agent.clone(),
2319 &["research".to_string()],
2320 ®istry,
2321 &test_ctx(),
2322 )
2323 .await;
2324
2325 assert_eq!(
2327 applied.runtime_agent.system_prompt,
2328 base_runtime_agent.system_prompt
2329 );
2330 assert!(applied.applied_ids.is_empty()); }
2332
2333 #[tokio::test]
2334 async fn test_apply_capabilities_multiple() {
2335 let registry = CapabilityRegistry::with_builtins();
2336 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2337
2338 let applied = apply_capabilities(
2339 base_runtime_agent.clone(),
2340 &["noop".to_string(), "current_time".to_string()],
2341 ®istry,
2342 &test_ctx(),
2343 )
2344 .await;
2345
2346 assert!(applied.tool_registry.has("get_current_time"));
2347 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
2348 }
2349
2350 #[tokio::test]
2351 async fn test_apply_capabilities_preserves_order() {
2352 let registry = CapabilityRegistry::with_builtins();
2353 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
2354
2355 let applied = apply_capabilities(
2357 base_runtime_agent,
2358 &["current_time".to_string(), "noop".to_string()],
2359 ®istry,
2360 &test_ctx(),
2361 )
2362 .await;
2363
2364 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
2365 }
2366
2367 #[tokio::test]
2368 async fn test_apply_capabilities_test_math() {
2369 let registry = CapabilityRegistry::with_builtins();
2370 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2371
2372 let applied = apply_capabilities(
2373 base_runtime_agent.clone(),
2374 &["test_math".to_string()],
2375 ®istry,
2376 &test_ctx(),
2377 )
2378 .await;
2379
2380 assert!(
2382 !applied
2383 .runtime_agent
2384 .system_prompt
2385 .contains("<capability id=\"test_math\">")
2386 );
2387 assert!(
2389 applied
2390 .runtime_agent
2391 .system_prompt
2392 .contains("You are a helpful assistant.")
2393 );
2394 assert!(applied.tool_registry.has("add"));
2395 assert!(applied.tool_registry.has("subtract"));
2396 assert!(applied.tool_registry.has("multiply"));
2397 assert!(applied.tool_registry.has("divide"));
2398 assert_eq!(applied.tool_registry.len(), 4);
2399 }
2400
2401 #[tokio::test]
2402 async fn test_apply_capabilities_test_weather() {
2403 let registry = CapabilityRegistry::with_builtins();
2404 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2405
2406 let applied = apply_capabilities(
2407 base_runtime_agent.clone(),
2408 &["test_weather".to_string()],
2409 ®istry,
2410 &test_ctx(),
2411 )
2412 .await;
2413
2414 assert!(
2416 !applied
2417 .runtime_agent
2418 .system_prompt
2419 .contains("<capability id=\"test_weather\">")
2420 );
2421 assert!(applied.tool_registry.has("get_weather"));
2422 assert!(applied.tool_registry.has("get_forecast"));
2423 assert_eq!(applied.tool_registry.len(), 2);
2424 }
2425
2426 #[tokio::test]
2427 async fn test_apply_capabilities_test_math_and_test_weather() {
2428 let registry = CapabilityRegistry::with_builtins();
2429 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2430
2431 let applied = apply_capabilities(
2432 base_runtime_agent.clone(),
2433 &["test_math".to_string(), "test_weather".to_string()],
2434 ®istry,
2435 &test_ctx(),
2436 )
2437 .await;
2438
2439 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
2442 assert!(applied.tool_registry.has("get_weather"));
2443 }
2444
2445 #[tokio::test]
2446 async fn test_apply_capabilities_stateless_todo_list() {
2447 let registry = CapabilityRegistry::with_builtins();
2448 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2449
2450 let applied = apply_capabilities(
2451 base_runtime_agent.clone(),
2452 &["stateless_todo_list".to_string()],
2453 ®istry,
2454 &test_ctx(),
2455 )
2456 .await;
2457
2458 assert!(
2460 applied
2461 .runtime_agent
2462 .system_prompt
2463 .contains("Task Management")
2464 );
2465 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
2466 assert!(applied.tool_registry.has("write_todos"));
2467 assert_eq!(applied.tool_registry.len(), 1);
2468 }
2469
2470 #[tokio::test]
2471 async fn test_apply_capabilities_web_fetch() {
2472 let registry = CapabilityRegistry::with_builtins();
2473 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2474
2475 let applied = apply_capabilities(
2476 base_runtime_agent.clone(),
2477 &["web_fetch".to_string()],
2478 ®istry,
2479 &test_ctx(),
2480 )
2481 .await;
2482
2483 assert!(
2485 applied
2486 .runtime_agent
2487 .system_prompt
2488 .contains(&base_runtime_agent.system_prompt)
2489 );
2490 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
2491 assert!(applied.tool_registry.has("web_fetch"));
2492 assert_eq!(applied.tool_registry.len(), 1);
2493 }
2494
2495 #[tokio::test]
2500 async fn test_xml_tags_wrap_capability_prompts() {
2501 let registry = CapabilityRegistry::with_builtins();
2502 let collected =
2503 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
2504 .await;
2505
2506 assert_eq!(collected.system_prompt_parts.len(), 1);
2507 let part = &collected.system_prompt_parts[0];
2508 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
2509 assert!(part.ends_with("</capability>"));
2510 assert!(part.contains("Task Management"));
2511 }
2512
2513 #[tokio::test]
2514 async fn test_xml_tags_multiple_capabilities() {
2515 let registry = CapabilityRegistry::with_builtins();
2516 let collected = collect_capabilities(
2517 &[
2518 "stateless_todo_list".to_string(),
2519 "session_schedule".to_string(),
2520 ],
2521 ®istry,
2522 &test_ctx(),
2523 )
2524 .await;
2525
2526 assert_eq!(collected.system_prompt_parts.len(), 2);
2527 assert!(
2528 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
2529 );
2530 assert!(
2531 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
2532 );
2533
2534 let prefix = collected.system_prompt_prefix().unwrap();
2535 assert!(prefix.contains("</capability>\n\n<capability"));
2537 }
2538
2539 #[tokio::test]
2540 async fn test_xml_tags_system_prompt_wrapping() {
2541 let registry = CapabilityRegistry::with_builtins();
2542 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
2543
2544 let applied = apply_capabilities(
2545 base,
2546 &["stateless_todo_list".to_string()],
2547 ®istry,
2548 &test_ctx(),
2549 )
2550 .await;
2551
2552 let prompt = &applied.runtime_agent.system_prompt;
2553 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
2555 assert!(prompt.contains("</capability>"));
2556 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
2558 }
2559
2560 #[tokio::test]
2561 async fn test_no_xml_wrapping_without_capabilities() {
2562 let registry = CapabilityRegistry::with_builtins();
2563 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
2564
2565 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
2566
2567 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
2569 assert!(
2570 !applied
2571 .runtime_agent
2572 .system_prompt
2573 .contains("<system-prompt>")
2574 );
2575 }
2576
2577 #[tokio::test]
2578 async fn test_no_xml_wrapping_for_noop_capability() {
2579 let registry = CapabilityRegistry::with_builtins();
2580 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
2581
2582 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
2584
2585 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
2586 assert!(
2587 !applied
2588 .runtime_agent
2589 .system_prompt
2590 .contains("<system-prompt>")
2591 );
2592 }
2593
2594 #[tokio::test]
2599 async fn test_collect_capabilities_includes_mounts() {
2600 let registry = CapabilityRegistry::with_builtins();
2601
2602 let collected =
2603 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
2604
2605 assert!(!collected.mounts.is_empty());
2606 assert_eq!(collected.mounts.len(), 1);
2607 assert_eq!(collected.mounts[0].path, "/samples");
2608 assert!(collected.mounts[0].is_readonly());
2609 }
2610
2611 #[tokio::test]
2612 async fn test_collect_capabilities_empty_mounts_by_default() {
2613 let registry = CapabilityRegistry::with_builtins();
2614
2615 let collected =
2617 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
2618
2619 assert!(collected.mounts.is_empty());
2620 }
2621
2622 #[tokio::test]
2623 async fn test_collect_capabilities_combines_mounts() {
2624 let registry = CapabilityRegistry::with_builtins();
2625
2626 let collected = collect_capabilities(
2629 &["sample_data".to_string(), "current_time".to_string()],
2630 ®istry,
2631 &test_ctx(),
2632 )
2633 .await;
2634
2635 assert_eq!(collected.mounts.len(), 1);
2636 assert!(
2638 collected
2639 .applied_ids
2640 .iter()
2641 .any(|id| id == "session_file_system")
2642 );
2643 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
2644 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
2645 }
2646
2647 #[test]
2648 fn test_sample_data_capability() {
2649 let registry = CapabilityRegistry::with_builtins();
2650 let cap = registry.get("sample_data").unwrap();
2651
2652 assert_eq!(cap.id(), "sample_data");
2653 assert_eq!(cap.name(), "Sample Data");
2654 assert_eq!(cap.status(), CapabilityStatus::Available);
2655
2656 assert!(cap.system_prompt_addition().is_some());
2658 assert!(cap.tools().is_empty());
2659
2660 assert!(!cap.mounts().is_empty());
2662 }
2663
2664 #[test]
2669 fn test_resolve_dependencies_empty() {
2670 let registry = CapabilityRegistry::with_builtins();
2671
2672 let resolved = resolve_dependencies(&[], ®istry).unwrap();
2673
2674 assert!(resolved.resolved_ids.is_empty());
2675 assert!(resolved.added_as_dependencies.is_empty());
2676 assert!(resolved.user_selected.is_empty());
2677 }
2678
2679 #[test]
2680 fn test_resolve_dependencies_no_deps() {
2681 let registry = CapabilityRegistry::with_builtins();
2682
2683 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
2685
2686 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
2687 assert!(resolved.added_as_dependencies.is_empty());
2688 }
2689
2690 #[test]
2691 fn test_resolve_dependencies_with_deps() {
2692 let registry = CapabilityRegistry::with_builtins();
2693
2694 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
2696
2697 assert_eq!(resolved.resolved_ids.len(), 2);
2699 let fs_pos = resolved
2700 .resolved_ids
2701 .iter()
2702 .position(|id| id == "session_file_system")
2703 .unwrap();
2704 let sd_pos = resolved
2705 .resolved_ids
2706 .iter()
2707 .position(|id| id == "sample_data")
2708 .unwrap();
2709 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
2710
2711 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
2713 }
2714
2715 #[test]
2716 fn test_resolve_dependencies_already_selected() {
2717 let registry = CapabilityRegistry::with_builtins();
2718
2719 let resolved = resolve_dependencies(
2721 &["session_file_system".to_string(), "sample_data".to_string()],
2722 ®istry,
2723 )
2724 .unwrap();
2725
2726 assert_eq!(resolved.resolved_ids.len(), 2);
2727 assert!(resolved.added_as_dependencies.is_empty());
2729 }
2730
2731 #[test]
2732 fn test_resolve_dependencies_preserves_order() {
2733 let registry = CapabilityRegistry::with_builtins();
2734
2735 let resolved =
2737 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
2738 .unwrap();
2739
2740 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
2741 }
2742
2743 #[test]
2744 fn test_resolve_dependencies_unknown_capability() {
2745 let registry = CapabilityRegistry::with_builtins();
2746
2747 let resolved =
2749 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
2750
2751 assert!(resolved.resolved_ids.is_empty());
2752 }
2753
2754 #[test]
2755 fn test_get_dependencies() {
2756 let registry = CapabilityRegistry::with_builtins();
2757
2758 let deps = get_dependencies("sample_data", ®istry);
2760 assert_eq!(deps, vec!["session_file_system"]);
2761
2762 let deps = get_dependencies("current_time", ®istry);
2764 assert!(deps.is_empty());
2765
2766 let deps = get_dependencies("unknown", ®istry);
2768 assert!(deps.is_empty());
2769 }
2770
2771 #[test]
2772 fn test_sample_data_has_dependency() {
2773 let registry = CapabilityRegistry::with_builtins();
2774 let cap = registry.get("sample_data").unwrap();
2775
2776 let deps = cap.dependencies();
2777 assert_eq!(deps.len(), 1);
2778 assert_eq!(deps[0], "session_file_system");
2779 }
2780
2781 #[test]
2782 fn test_noop_has_no_dependencies() {
2783 let registry = CapabilityRegistry::with_builtins();
2784 let cap = registry.get("noop").unwrap();
2785
2786 assert!(cap.dependencies().is_empty());
2787 }
2788
2789 #[test]
2793 fn test_circular_dependency_error() {
2794 struct CapA;
2796 struct CapB;
2797
2798 impl Capability for CapA {
2799 fn id(&self) -> &str {
2800 "test_cap_a"
2801 }
2802 fn name(&self) -> &str {
2803 "Test A"
2804 }
2805 fn description(&self) -> &str {
2806 "Test capability A"
2807 }
2808 fn dependencies(&self) -> Vec<&'static str> {
2809 vec!["test_cap_b"]
2810 }
2811 }
2812
2813 impl Capability for CapB {
2814 fn id(&self) -> &str {
2815 "test_cap_b"
2816 }
2817 fn name(&self) -> &str {
2818 "Test B"
2819 }
2820 fn description(&self) -> &str {
2821 "Test capability B"
2822 }
2823 fn dependencies(&self) -> Vec<&'static str> {
2824 vec!["test_cap_a"]
2825 }
2826 }
2827
2828 let mut registry = CapabilityRegistry::new();
2829 registry.register(CapA);
2830 registry.register(CapB);
2831
2832 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
2833
2834 assert!(result.is_err());
2835 match result.unwrap_err() {
2836 DependencyError::CircularDependency { capability_id, .. } => {
2837 assert_eq!(capability_id, "test_cap_a");
2838 }
2839 _ => panic!("Expected CircularDependency error"),
2840 }
2841 }
2842
2843 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
2848
2849 struct FilterTestCapability {
2851 priority: i32,
2852 }
2853
2854 impl Capability for FilterTestCapability {
2855 fn id(&self) -> &str {
2856 "filter_test"
2857 }
2858 fn name(&self) -> &str {
2859 "Filter Test"
2860 }
2861 fn description(&self) -> &str {
2862 "Test capability with message filter"
2863 }
2864 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
2865 Some(Arc::new(FilterTestProvider {
2866 priority: self.priority,
2867 }))
2868 }
2869 }
2870
2871 struct FilterTestProvider {
2872 priority: i32,
2873 }
2874
2875 impl MessageFilterProvider for FilterTestProvider {
2876 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
2877 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
2879 query
2880 .filters
2881 .push(MessageFilter::Search(search.to_string()));
2882 }
2883 }
2884
2885 fn priority(&self) -> i32 {
2886 self.priority
2887 }
2888 }
2889
2890 #[tokio::test]
2891 async fn test_collect_capabilities_with_configs_no_filter_providers() {
2892 let registry = CapabilityRegistry::with_builtins();
2893 let configs = vec![AgentCapabilityConfig {
2894 capability_ref: CapabilityId::new("current_time"),
2895 config: serde_json::json!({}),
2896 }];
2897
2898 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
2899
2900 assert!(collected.message_filter_providers.is_empty());
2901 assert!(!collected.has_message_filters());
2902 }
2903
2904 #[tokio::test]
2905 async fn test_collect_capabilities_with_configs_with_filter_provider() {
2906 let mut registry = CapabilityRegistry::new();
2907 registry.register(FilterTestCapability { priority: 0 });
2908
2909 let configs = vec![AgentCapabilityConfig {
2910 capability_ref: CapabilityId::new("filter_test"),
2911 config: serde_json::json!({ "search": "hello" }),
2912 }];
2913
2914 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
2915
2916 assert_eq!(collected.message_filter_providers.len(), 1);
2917 assert!(collected.has_message_filters());
2918 }
2919
2920 #[tokio::test]
2921 async fn test_collect_capabilities_with_configs_filter_priority_order() {
2922 struct HighPriorityCapability;
2924 struct LowPriorityCapability;
2925
2926 impl Capability for HighPriorityCapability {
2927 fn id(&self) -> &str {
2928 "high_priority"
2929 }
2930 fn name(&self) -> &str {
2931 "High Priority"
2932 }
2933 fn description(&self) -> &str {
2934 "Test"
2935 }
2936 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
2937 Some(Arc::new(FilterTestProvider { priority: 10 }))
2938 }
2939 }
2940
2941 impl Capability for LowPriorityCapability {
2942 fn id(&self) -> &str {
2943 "low_priority"
2944 }
2945 fn name(&self) -> &str {
2946 "Low Priority"
2947 }
2948 fn description(&self) -> &str {
2949 "Test"
2950 }
2951 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
2952 Some(Arc::new(FilterTestProvider { priority: -5 }))
2953 }
2954 }
2955
2956 let mut registry = CapabilityRegistry::new();
2957 registry.register(HighPriorityCapability);
2958 registry.register(LowPriorityCapability);
2959
2960 let configs = vec![
2962 AgentCapabilityConfig {
2963 capability_ref: CapabilityId::new("high_priority"),
2964 config: serde_json::json!({}),
2965 },
2966 AgentCapabilityConfig {
2967 capability_ref: CapabilityId::new("low_priority"),
2968 config: serde_json::json!({}),
2969 },
2970 ];
2971
2972 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
2973
2974 assert_eq!(collected.message_filter_providers.len(), 2);
2976 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
2977 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
2978 }
2979
2980 #[tokio::test]
2981 async fn test_collected_capabilities_apply_message_filters() {
2982 let mut registry = CapabilityRegistry::new();
2983 registry.register(FilterTestCapability { priority: 0 });
2984
2985 let configs = vec![AgentCapabilityConfig {
2986 capability_ref: CapabilityId::new("filter_test"),
2987 config: serde_json::json!({ "search": "test_query" }),
2988 }];
2989
2990 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
2991
2992 let session_id: SessionId = Uuid::now_v7().into();
2994 let mut query = MessageQuery::new(session_id);
2995
2996 collected.apply_message_filters(&mut query);
2997
2998 assert_eq!(query.filters.len(), 1);
3000 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3001 }
3002
3003 #[tokio::test]
3004 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3005 struct SearchCapability {
3006 id: &'static str,
3007 search_term: &'static str,
3008 priority: i32,
3009 }
3010
3011 struct SearchProvider {
3012 search_term: &'static str,
3013 priority: i32,
3014 }
3015
3016 impl MessageFilterProvider for SearchProvider {
3017 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3018 query
3019 .filters
3020 .push(MessageFilter::Search(self.search_term.to_string()));
3021 }
3022
3023 fn priority(&self) -> i32 {
3024 self.priority
3025 }
3026 }
3027
3028 impl Capability for SearchCapability {
3029 fn id(&self) -> &str {
3030 self.id
3031 }
3032 fn name(&self) -> &str {
3033 "Search"
3034 }
3035 fn description(&self) -> &str {
3036 "Test"
3037 }
3038 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3039 Some(Arc::new(SearchProvider {
3040 search_term: self.search_term,
3041 priority: self.priority,
3042 }))
3043 }
3044 }
3045
3046 let mut registry = CapabilityRegistry::new();
3047 registry.register(SearchCapability {
3048 id: "cap_a",
3049 search_term: "alpha",
3050 priority: 5,
3051 });
3052 registry.register(SearchCapability {
3053 id: "cap_b",
3054 search_term: "beta",
3055 priority: 1,
3056 });
3057 registry.register(SearchCapability {
3058 id: "cap_c",
3059 search_term: "gamma",
3060 priority: 10,
3061 });
3062
3063 let configs = vec![
3064 AgentCapabilityConfig {
3065 capability_ref: CapabilityId::new("cap_a"),
3066 config: serde_json::json!({}),
3067 },
3068 AgentCapabilityConfig {
3069 capability_ref: CapabilityId::new("cap_b"),
3070 config: serde_json::json!({}),
3071 },
3072 AgentCapabilityConfig {
3073 capability_ref: CapabilityId::new("cap_c"),
3074 config: serde_json::json!({}),
3075 },
3076 ];
3077
3078 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3079
3080 let session_id: SessionId = Uuid::now_v7().into();
3081 let mut query = MessageQuery::new(session_id);
3082
3083 collected.apply_message_filters(&mut query);
3084
3085 assert_eq!(query.filters.len(), 3);
3087 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3088 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3089 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3090 }
3091
3092 #[test]
3093 fn test_capability_without_message_filter_returns_none() {
3094 let registry = CapabilityRegistry::with_builtins();
3095
3096 let noop = registry.get("noop").unwrap();
3097 assert!(noop.message_filter_provider().is_none());
3098
3099 let current_time = registry.get("current_time").unwrap();
3100 assert!(current_time.message_filter_provider().is_none());
3101 }
3102
3103 #[tokio::test]
3104 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3105 let mut registry = CapabilityRegistry::new();
3106 registry.register(FilterTestCapability { priority: 0 });
3107
3108 let test_config = serde_json::json!({
3109 "search": "custom_search",
3110 "extra_field": 42
3111 });
3112
3113 let configs = vec![AgentCapabilityConfig {
3114 capability_ref: CapabilityId::new("filter_test"),
3115 config: test_config.clone(),
3116 }];
3117
3118 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3119
3120 assert_eq!(collected.message_filter_providers.len(), 1);
3122 let (_, stored_config) = &collected.message_filter_providers[0];
3123 assert_eq!(*stored_config, test_config);
3124 }
3125
3126 #[test]
3131 fn test_collect_message_filters_only_collects_filters() {
3132 let mut registry = CapabilityRegistry::new();
3133 registry.register(FilterTestCapability { priority: 0 });
3134
3135 let configs = vec![AgentCapabilityConfig {
3136 capability_ref: CapabilityId::new("filter_test"),
3137 config: serde_json::json!({ "search": "test_query" }),
3138 }];
3139
3140 let collected = collect_message_filters_only(&configs, ®istry);
3141
3142 let session_id: SessionId = Uuid::now_v7().into();
3143 let mut query = MessageQuery::new(session_id);
3144 collected.apply_message_filters(&mut query);
3145
3146 assert_eq!(query.filters.len(), 1);
3147 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3148 }
3149
3150 #[test]
3151 fn test_collect_message_filters_only_skips_unknown_capabilities() {
3152 let registry = CapabilityRegistry::new();
3153
3154 let configs = vec![AgentCapabilityConfig {
3155 capability_ref: CapabilityId::new("nonexistent"),
3156 config: serde_json::json!({}),
3157 }];
3158
3159 let collected = collect_message_filters_only(&configs, ®istry);
3160 assert!(collected.message_filter_providers.is_empty());
3161 }
3162
3163 #[test]
3164 fn test_collect_message_filters_only_preserves_priority_order() {
3165 struct PriorityFilterCap {
3166 id: &'static str,
3167 search_term: &'static str,
3168 priority: i32,
3169 }
3170
3171 struct PriorityFilterProvider {
3172 search_term: &'static str,
3173 priority: i32,
3174 }
3175
3176 impl Capability for PriorityFilterCap {
3177 fn id(&self) -> &str {
3178 self.id
3179 }
3180 fn name(&self) -> &str {
3181 self.id
3182 }
3183 fn description(&self) -> &str {
3184 "priority test"
3185 }
3186 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3187 Some(Arc::new(PriorityFilterProvider {
3188 search_term: self.search_term,
3189 priority: self.priority,
3190 }))
3191 }
3192 }
3193
3194 impl MessageFilterProvider for PriorityFilterProvider {
3195 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3196 query
3197 .filters
3198 .push(MessageFilter::Search(self.search_term.to_string()));
3199 }
3200 fn priority(&self) -> i32 {
3201 self.priority
3202 }
3203 }
3204
3205 let mut registry = CapabilityRegistry::new();
3206 registry.register(PriorityFilterCap {
3207 id: "gamma",
3208 search_term: "gamma",
3209 priority: 10,
3210 });
3211 registry.register(PriorityFilterCap {
3212 id: "alpha",
3213 search_term: "alpha",
3214 priority: 5,
3215 });
3216 registry.register(PriorityFilterCap {
3217 id: "beta",
3218 search_term: "beta",
3219 priority: 1,
3220 });
3221
3222 let configs = vec![
3223 AgentCapabilityConfig {
3224 capability_ref: CapabilityId::new("gamma"),
3225 config: serde_json::json!({}),
3226 },
3227 AgentCapabilityConfig {
3228 capability_ref: CapabilityId::new("alpha"),
3229 config: serde_json::json!({}),
3230 },
3231 AgentCapabilityConfig {
3232 capability_ref: CapabilityId::new("beta"),
3233 config: serde_json::json!({}),
3234 },
3235 ];
3236
3237 let collected = collect_message_filters_only(&configs, ®istry);
3238
3239 let session_id: SessionId = Uuid::now_v7().into();
3240 let mut query = MessageQuery::new(session_id);
3241 collected.apply_message_filters(&mut query);
3242
3243 assert_eq!(query.filters.len(), 3);
3245 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3246 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3247 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3248 }
3249
3250 #[test]
3251 fn test_collect_message_filters_only_post_load_invoked() {
3252 use crate::message::Message;
3253
3254 struct PostLoadCap;
3255 struct PostLoadProvider;
3256
3257 impl Capability for PostLoadCap {
3258 fn id(&self) -> &str {
3259 "post_load_test"
3260 }
3261 fn name(&self) -> &str {
3262 "PostLoad Test"
3263 }
3264 fn description(&self) -> &str {
3265 "test"
3266 }
3267 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3268 Some(Arc::new(PostLoadProvider))
3269 }
3270 }
3271
3272 impl MessageFilterProvider for PostLoadProvider {
3273 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
3274 fn priority(&self) -> i32 {
3275 0
3276 }
3277 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
3278 messages.reverse();
3280 }
3281 }
3282
3283 let mut registry = CapabilityRegistry::new();
3284 registry.register(PostLoadCap);
3285
3286 let configs = vec![AgentCapabilityConfig {
3287 capability_ref: CapabilityId::new("post_load_test"),
3288 config: serde_json::json!({}),
3289 }];
3290
3291 let collected = collect_message_filters_only(&configs, ®istry);
3292
3293 let mut messages = vec![Message::user("first"), Message::user("second")];
3294 collected.apply_post_load_filters(&mut messages);
3295
3296 assert_eq!(messages[0].text(), Some("second"));
3298 assert_eq!(messages[1].text(), Some("first"));
3299 }
3300
3301 #[test]
3302 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
3303 use crate::tool_types::ToolCall;
3304
3305 fn tool_heavy_messages() -> Vec<Message> {
3306 let mut messages = vec![Message::user("inspect files repeatedly")];
3307 for index in 0..9 {
3308 let call_id = format!("call_{index}");
3309 messages.push(Message::assistant_with_tools(
3310 "",
3311 vec![ToolCall {
3312 id: call_id.clone(),
3313 name: "read_file".to_string(),
3314 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
3315 }],
3316 ));
3317 messages.push(Message::tool_result(
3318 call_id,
3319 Some(serde_json::json!({
3320 "path": "/workspace/src/lib.rs",
3321 "content": format!("{}{}", "large file line\n".repeat(1000), index),
3322 "total_lines": 1000,
3323 "lines_shown": {"start": 1, "end": 1000},
3324 "truncated": false
3325 })),
3326 None,
3327 ));
3328 }
3329 messages
3330 }
3331
3332 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
3333 messages[2]
3334 .tool_result_content()
3335 .and_then(|result| result.result.as_ref())
3336 .and_then(|result| result.get("masked"))
3337 .and_then(|masked| masked.as_bool())
3338 .unwrap_or(false)
3339 }
3340
3341 let mut registry = CapabilityRegistry::new();
3342 registry.register(CompactionCapability);
3343 let context = ModelViewContext {
3344 session_id: SessionId::new(),
3345 prior_usage: None,
3346 };
3347
3348 let no_compaction = collect_model_view_providers(&[], ®istry);
3349 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
3350 assert!(!first_tool_result_is_masked(&unmasked));
3351
3352 let compaction = collect_model_view_providers(
3353 &[AgentCapabilityConfig {
3354 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3355 config: serde_json::json!({}),
3356 }],
3357 ®istry,
3358 );
3359 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
3360 assert!(first_tool_result_is_masked(&masked));
3361 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
3362 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
3363 }
3364
3365 #[tokio::test]
3375 async fn test_virtual_bash_capability_produces_bash_tool() {
3376 let registry = CapabilityRegistry::with_builtins();
3377 let collected =
3378 collect_capabilities(&["virtual_bash".to_string()], ®istry, &test_ctx()).await;
3379
3380 let tool_names: Vec<&str> = collected
3381 .tool_definitions
3382 .iter()
3383 .map(|t| t.name())
3384 .collect();
3385 assert!(
3386 tool_names.contains(&"bash"),
3387 "virtual_bash capability must produce 'bash' tool, got: {:?}",
3388 tool_names
3389 );
3390 assert!(
3391 !collected.tools.is_empty(),
3392 "virtual_bash must provide tool implementations"
3393 );
3394 }
3395
3396 #[tokio::test]
3397 async fn test_generic_harness_capability_set_produces_bash_tool() {
3398 let generic_harness_caps = vec![
3401 "session_file_system".to_string(),
3402 "virtual_bash".to_string(),
3403 "web_fetch".to_string(),
3404 "session_storage".to_string(),
3405 "session".to_string(),
3406 "agent_instructions".to_string(),
3407 "skills".to_string(),
3408 "infinity_context".to_string(),
3409 "openai_tool_search".to_string(),
3410 ];
3411
3412 let registry = CapabilityRegistry::with_builtins();
3413 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
3414
3415 let tool_names: Vec<&str> = collected
3416 .tool_definitions
3417 .iter()
3418 .map(|t| t.name())
3419 .collect();
3420 assert!(
3421 tool_names.contains(&"bash"),
3422 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
3423 tool_names
3424 );
3425 }
3426
3427 #[tokio::test]
3428 async fn test_collect_capabilities_tool_count_matches_definitions() {
3429 let registry = CapabilityRegistry::with_builtins();
3432 let collected =
3433 collect_capabilities(&["virtual_bash".to_string()], ®istry, &test_ctx()).await;
3434
3435 assert_eq!(
3436 collected.tools.len(),
3437 collected.tool_definitions.len(),
3438 "tool implementations ({}) must match tool definitions ({})",
3439 collected.tools.len(),
3440 collected.tool_definitions.len(),
3441 );
3442 }
3443
3444 #[tokio::test]
3448 async fn test_collect_capabilities_resolves_dependencies() {
3449 let registry = CapabilityRegistry::with_builtins();
3452 let collected =
3453 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3454
3455 assert!(
3457 collected
3458 .applied_ids
3459 .iter()
3460 .any(|id| id == "session_file_system"),
3461 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
3462 collected.applied_ids
3463 );
3464
3465 let tool_names: Vec<&str> = collected
3466 .tool_definitions
3467 .iter()
3468 .map(|t| t.name())
3469 .collect();
3470
3471 assert!(
3473 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
3474 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
3475 tool_names
3476 );
3477
3478 assert_eq!(
3480 collected.tools.len(),
3481 collected.tool_definitions.len(),
3482 "dependency-added tools must have implementations, not just definitions"
3483 );
3484 }
3485
3486 #[test]
3487 fn test_defaults_do_not_include_bash() {
3488 let registry = crate::ToolRegistry::with_defaults();
3491 assert!(
3492 !registry.has("bash"),
3493 "with_defaults() must not include 'bash' — it comes from virtual_bash capability"
3494 );
3495 }
3496
3497 #[tokio::test]
3504 async fn test_background_execution_auto_activates_with_virtual_bash() {
3505 let registry = CapabilityRegistry::with_builtins();
3506 let collected =
3507 collect_capabilities(&["virtual_bash".to_string()], ®istry, &test_ctx()).await;
3508
3509 let tool_names: Vec<&str> = collected
3510 .tool_definitions
3511 .iter()
3512 .map(|t| t.name())
3513 .collect();
3514 assert!(
3515 tool_names.contains(&"spawn_background"),
3516 "spawn_background must be auto-activated when virtual_bash (a \
3517 background-capable tool) is in the agent's capability set; got: {:?}",
3518 tool_names
3519 );
3520 assert!(
3521 collected
3522 .applied_ids
3523 .iter()
3524 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
3525 "background_execution must be in applied_ids when auto-activated; \
3526 got: {:?}",
3527 collected.applied_ids
3528 );
3529
3530 assert!(
3532 collected
3533 .tools
3534 .iter()
3535 .any(|t| t.name() == "spawn_background"),
3536 "spawn_background tool implementation must be present alongside the \
3537 definition (lockstep contract)"
3538 );
3539 }
3540
3541 #[tokio::test]
3544 async fn test_background_execution_does_not_auto_activate_without_hint() {
3545 let registry = CapabilityRegistry::with_builtins();
3546 let collected =
3548 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3549
3550 let tool_names: Vec<&str> = collected
3551 .tool_definitions
3552 .iter()
3553 .map(|t| t.name())
3554 .collect();
3555 assert!(
3556 !tool_names.contains(&"spawn_background"),
3557 "spawn_background must NOT be activated without a background-capable \
3558 tool; got: {:?}",
3559 tool_names
3560 );
3561 assert!(
3562 !collected
3563 .applied_ids
3564 .iter()
3565 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
3566 "background_execution must not appear in applied_ids when no \
3567 background-capable tool is present; got: {:?}",
3568 collected.applied_ids
3569 );
3570 }
3571
3572 #[tokio::test]
3576 async fn test_background_execution_explicit_selection_is_idempotent() {
3577 let registry = CapabilityRegistry::with_builtins();
3578 let collected = collect_capabilities(
3579 &[
3580 "virtual_bash".to_string(),
3581 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
3582 ],
3583 ®istry,
3584 &test_ctx(),
3585 )
3586 .await;
3587
3588 let spawn_background_count = collected
3589 .tool_definitions
3590 .iter()
3591 .filter(|t| t.name() == "spawn_background")
3592 .count();
3593 assert_eq!(
3594 spawn_background_count, 1,
3595 "spawn_background must appear exactly once even when \
3596 background_execution is selected explicitly alongside a \
3597 background-capable tool"
3598 );
3599 let applied_count = collected
3600 .applied_ids
3601 .iter()
3602 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
3603 .count();
3604 assert_eq!(
3605 applied_count, 1,
3606 "background_execution must appear exactly once in applied_ids"
3607 );
3608 }
3609
3610 #[test]
3615 fn test_defaults_do_not_include_spawn_background() {
3616 let registry = crate::ToolRegistry::with_defaults();
3617 assert!(
3618 !registry.has("spawn_background"),
3619 "with_defaults() must not include 'spawn_background' — it comes \
3620 from the background_execution capability (EVE-501)"
3621 );
3622 }
3623
3624 #[test]
3629 fn test_capability_features_default_empty() {
3630 let registry = CapabilityRegistry::with_builtins();
3631
3632 let noop = registry.get("noop").unwrap();
3634 assert!(noop.features().is_empty());
3635
3636 let current_time = registry.get("current_time").unwrap();
3637 assert!(current_time.features().is_empty());
3638 }
3639
3640 #[test]
3641 fn test_file_system_capability_features() {
3642 let registry = CapabilityRegistry::with_builtins();
3643
3644 let fs = registry.get("session_file_system").unwrap();
3645 assert_eq!(fs.features(), vec!["file_system"]);
3646 }
3647
3648 #[test]
3649 fn test_virtual_bash_capability_features() {
3650 let registry = CapabilityRegistry::with_builtins();
3651
3652 let bash = registry.get("virtual_bash").unwrap();
3653 assert_eq!(bash.features(), vec!["file_system"]);
3654 }
3655
3656 #[test]
3657 fn test_session_storage_capability_features() {
3658 let registry = CapabilityRegistry::with_builtins();
3659
3660 let storage = registry.get("session_storage").unwrap();
3661 let features = storage.features();
3662 assert!(features.contains(&"secrets"));
3663 assert!(features.contains(&"key_value"));
3664 }
3665
3666 #[test]
3667 fn test_session_schedule_capability_features() {
3668 let registry = CapabilityRegistry::with_builtins();
3669
3670 let schedule = registry.get("session_schedule").unwrap();
3671 assert_eq!(schedule.features(), vec!["schedules"]);
3672 }
3673
3674 #[test]
3675 fn test_session_sql_database_capability_features() {
3676 let registry = CapabilityRegistry::with_builtins();
3677
3678 let sql = registry.get("session_sql_database").unwrap();
3679 assert_eq!(sql.features(), vec!["sql_database"]);
3680 }
3681
3682 #[test]
3683 fn test_sample_data_capability_features() {
3684 let registry = CapabilityRegistry::with_builtins();
3685
3686 let sample = registry.get("sample_data").unwrap();
3687 assert_eq!(sample.features(), vec!["file_system"]);
3688 }
3689
3690 #[test]
3691 fn test_compute_features_empty() {
3692 let registry = CapabilityRegistry::with_builtins();
3693
3694 let features = compute_features(&[], ®istry);
3695 assert!(features.is_empty());
3696 }
3697
3698 #[test]
3699 fn test_compute_features_single_capability() {
3700 let registry = CapabilityRegistry::with_builtins();
3701
3702 let features = compute_features(&["session_schedule".to_string()], ®istry);
3703 assert_eq!(features, vec!["schedules"]);
3704 }
3705
3706 #[test]
3707 fn test_compute_features_multiple_capabilities() {
3708 let registry = CapabilityRegistry::with_builtins();
3709
3710 let features = compute_features(
3711 &[
3712 "session_file_system".to_string(),
3713 "session_storage".to_string(),
3714 "session_schedule".to_string(),
3715 ],
3716 ®istry,
3717 );
3718 assert!(features.contains(&"file_system".to_string()));
3719 assert!(features.contains(&"secrets".to_string()));
3720 assert!(features.contains(&"key_value".to_string()));
3721 assert!(features.contains(&"schedules".to_string()));
3722 }
3723
3724 #[test]
3725 fn test_compute_features_deduplicates() {
3726 let registry = CapabilityRegistry::with_builtins();
3727
3728 let features = compute_features(
3730 &[
3731 "session_file_system".to_string(),
3732 "virtual_bash".to_string(),
3733 ],
3734 ®istry,
3735 );
3736 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
3737 assert_eq!(file_system_count, 1, "file_system should appear only once");
3738 }
3739
3740 #[test]
3741 fn test_compute_features_includes_dependency_features() {
3742 let registry = CapabilityRegistry::with_builtins();
3743
3744 let features = compute_features(&["virtual_bash".to_string()], ®istry);
3746 assert!(features.contains(&"file_system".to_string()));
3747 }
3748
3749 #[test]
3750 fn test_compute_features_generic_harness_set() {
3751 let registry = CapabilityRegistry::with_builtins();
3752
3753 let features = compute_features(
3755 &[
3756 "session_file_system".to_string(),
3757 "virtual_bash".to_string(),
3758 "session_storage".to_string(),
3759 "session".to_string(),
3760 "session_schedule".to_string(),
3761 ],
3762 ®istry,
3763 );
3764 assert!(features.contains(&"file_system".to_string()));
3765 assert!(features.contains(&"secrets".to_string()));
3766 assert!(features.contains(&"key_value".to_string()));
3767 assert!(features.contains(&"schedules".to_string()));
3768 }
3769
3770 #[test]
3771 fn test_compute_features_unknown_capability_ignored() {
3772 let registry = CapabilityRegistry::with_builtins();
3773
3774 let features = compute_features(
3775 &["unknown_cap".to_string(), "session_schedule".to_string()],
3776 ®istry,
3777 );
3778 assert_eq!(features, vec!["schedules"]);
3779 }
3780
3781 #[test]
3782 fn test_risk_level_ordering() {
3783 assert!(RiskLevel::Low < RiskLevel::Medium);
3784 assert!(RiskLevel::Medium < RiskLevel::High);
3785 }
3786
3787 #[test]
3788 fn test_risk_level_serde_roundtrip() {
3789 let high = RiskLevel::High;
3790 let json = serde_json::to_string(&high).unwrap();
3791 assert_eq!(json, "\"high\"");
3792 let back: RiskLevel = serde_json::from_str(&json).unwrap();
3793 assert_eq!(back, RiskLevel::High);
3794 }
3795
3796 #[test]
3797 fn test_capability_risk_levels() {
3798 let registry = CapabilityRegistry::with_builtins();
3799
3800 let bash = registry.get("virtual_bash").unwrap();
3802 assert_eq!(bash.risk_level(), RiskLevel::High);
3803
3804 let fetch = registry.get("web_fetch").unwrap();
3806 assert_eq!(fetch.risk_level(), RiskLevel::High);
3807
3808 let noop = registry.get("noop").unwrap();
3810 assert_eq!(noop.risk_level(), RiskLevel::Low);
3811 }
3812
3813 #[tokio::test]
3818 async fn test_apply_capabilities_openai_tool_search() {
3819 let registry = CapabilityRegistry::with_builtins();
3820 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
3821
3822 let applied = apply_capabilities(
3823 base_runtime_agent.clone(),
3824 &["openai_tool_search".to_string()],
3825 ®istry,
3826 &test_ctx(),
3827 )
3828 .await;
3829
3830 assert_eq!(
3832 applied.runtime_agent.system_prompt,
3833 base_runtime_agent.system_prompt
3834 );
3835 assert!(applied.tool_registry.is_empty());
3836 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
3837
3838 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
3840 assert!(ts.enabled);
3841 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
3842 }
3843
3844 #[tokio::test]
3845 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
3846 let registry = CapabilityRegistry::with_builtins();
3847 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
3848
3849 let applied = apply_capabilities(
3850 base_runtime_agent,
3851 &[
3852 "current_time".to_string(),
3853 "openai_tool_search".to_string(),
3854 "test_math".to_string(),
3855 ],
3856 ®istry,
3857 &test_ctx(),
3858 )
3859 .await;
3860
3861 assert!(applied.tool_registry.has("get_current_time"));
3863 assert!(applied.tool_registry.has("add"));
3864 assert!(applied.tool_registry.has("subtract"));
3865 assert!(applied.tool_registry.has("multiply"));
3866 assert!(applied.tool_registry.has("divide"));
3867
3868 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
3870 assert!(ts.enabled);
3871 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
3872 }
3873
3874 #[tokio::test]
3875 async fn test_collect_capabilities_tool_search_custom_threshold() {
3876 let registry = CapabilityRegistry::with_builtins();
3877
3878 let configs = vec![AgentCapabilityConfig {
3879 capability_ref: CapabilityId::new("openai_tool_search"),
3880 config: serde_json::json!({"threshold": 5}),
3881 }];
3882
3883 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3884
3885 let ts = collected.tool_search.as_ref().unwrap();
3886 assert!(ts.enabled);
3887 assert_eq!(ts.threshold, 5);
3888 }
3889
3890 #[tokio::test]
3891 async fn test_collect_capabilities_no_tool_search_without_capability() {
3892 let registry = CapabilityRegistry::with_builtins();
3893
3894 let configs = vec![AgentCapabilityConfig {
3895 capability_ref: CapabilityId::new("current_time"),
3896 config: serde_json::json!({}),
3897 }];
3898
3899 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3900
3901 assert!(collected.tool_search.is_none());
3902 }
3903
3904 #[tokio::test]
3905 async fn test_collect_capabilities_tool_search_category_propagation() {
3906 let registry = CapabilityRegistry::with_builtins();
3907
3908 let configs = vec![
3910 AgentCapabilityConfig {
3911 capability_ref: CapabilityId::new("test_math"),
3912 config: serde_json::json!({}),
3913 },
3914 AgentCapabilityConfig {
3915 capability_ref: CapabilityId::new("openai_tool_search"),
3916 config: serde_json::json!({}),
3917 },
3918 ];
3919
3920 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3921
3922 assert!(collected.tool_search.is_some());
3924
3925 for tool_def in &collected.tool_definitions {
3927 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
3929 assert!(
3930 tool_def.category().is_some(),
3931 "Tool {} should have a category from its capability",
3932 tool_def.name()
3933 );
3934 }
3935 }
3936 }
3937
3938 #[tokio::test]
3939 async fn test_apply_capabilities_prompt_caching() {
3940 let registry = CapabilityRegistry::with_builtins();
3941 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
3942
3943 let applied = apply_capabilities(
3944 base_runtime_agent.clone(),
3945 &["prompt_caching".to_string()],
3946 ®istry,
3947 &test_ctx(),
3948 )
3949 .await;
3950
3951 assert_eq!(
3952 applied.runtime_agent.system_prompt,
3953 base_runtime_agent.system_prompt
3954 );
3955 assert!(applied.tool_registry.is_empty());
3956 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
3957
3958 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
3959 assert!(prompt_cache.enabled);
3960 assert_eq!(
3961 prompt_cache.strategy,
3962 crate::llm_driver_registry::PromptCacheStrategy::Auto
3963 );
3964 assert!(prompt_cache.gemini_cached_content.is_none());
3965 }
3966
3967 #[tokio::test]
3968 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
3969 let registry = CapabilityRegistry::with_builtins();
3970
3971 let configs = vec![AgentCapabilityConfig {
3972 capability_ref: CapabilityId::new("prompt_caching"),
3973 config: serde_json::json!({"strategy": "auto"}),
3974 }];
3975
3976 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3977
3978 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
3979 assert!(prompt_cache.enabled);
3980 assert_eq!(
3981 prompt_cache.strategy,
3982 crate::llm_driver_registry::PromptCacheStrategy::Auto
3983 );
3984 assert!(prompt_cache.gemini_cached_content.is_none());
3985 }
3986
3987 #[tokio::test]
3988 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
3989 let registry = CapabilityRegistry::with_builtins();
3990
3991 let configs = vec![AgentCapabilityConfig {
3992 capability_ref: CapabilityId::new("prompt_caching"),
3993 config: serde_json::json!({
3994 "strategy": "auto",
3995 "gemini_cached_content": "cachedContents/demo-cache"
3996 }),
3997 }];
3998
3999 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4000
4001 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
4002 assert_eq!(
4003 prompt_cache.gemini_cached_content.as_deref(),
4004 Some("cachedContents/demo-cache")
4005 );
4006 }
4007
4008 struct SkillContributingCapability;
4013
4014 impl Capability for SkillContributingCapability {
4015 fn id(&self) -> &str {
4016 "contributes_skills"
4017 }
4018 fn name(&self) -> &str {
4019 "Contributes Skills"
4020 }
4021 fn description(&self) -> &str {
4022 "Test capability that contributes skills."
4023 }
4024 fn contribute_skills(&self) -> Vec<SkillContribution> {
4025 vec![
4026 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
4027 .with_files(vec![(
4028 "scripts/a.sh".to_string(),
4029 "#!/bin/sh\necho a\n".to_string(),
4030 )]),
4031 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
4032 .with_user_invocable(false),
4033 ]
4034 }
4035 }
4036
4037 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
4038 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
4039 MountSource::InlineFile { content, .. } => content.as_str(),
4040 _ => panic!("Expected InlineFile for SKILL.md"),
4041 }
4042 }
4043
4044 #[tokio::test]
4045 async fn test_contribute_skills_normalized_to_mounts() {
4046 let mut registry = CapabilityRegistry::new();
4047 registry.register(SkillContributingCapability);
4048
4049 let configs = vec![AgentCapabilityConfig {
4050 capability_ref: CapabilityId::new("contributes_skills"),
4051 config: serde_json::json!({}),
4052 }];
4053
4054 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4055
4056 let skill_mounts: Vec<_> = collected
4057 .mounts
4058 .iter()
4059 .filter(|m| m.path.starts_with("/.agents/skills/"))
4060 .collect();
4061 assert_eq!(skill_mounts.len(), 2);
4062
4063 for m in &skill_mounts {
4066 assert!(m.is_readonly());
4067 assert_eq!(m.capability_id, "contributes_skills");
4068 }
4069
4070 let alpha = skill_mounts
4071 .iter()
4072 .find(|m| m.path == "/.agents/skills/alpha-skill")
4073 .expect("alpha-skill mount missing");
4074 match &alpha.source {
4075 MountSource::InlineDirectory { entries } => {
4076 assert!(entries.contains_key("SKILL.md"));
4077 assert!(entries.contains_key("scripts/a.sh"));
4078 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
4079 assert_eq!(parsed.name, "alpha-skill");
4080 assert!(parsed.user_invocable);
4081 }
4082 _ => panic!("Expected InlineDirectory"),
4083 }
4084
4085 let beta = skill_mounts
4086 .iter()
4087 .find(|m| m.path == "/.agents/skills/beta-skill")
4088 .expect("beta-skill mount missing");
4089 match &beta.source {
4090 MountSource::InlineDirectory { entries } => {
4091 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
4092 assert!(!parsed.user_invocable);
4093 }
4094 _ => panic!("Expected InlineDirectory"),
4095 }
4096 }
4097
4098 #[tokio::test]
4099 async fn test_contribute_skills_default_empty() {
4100 let mut registry = CapabilityRegistry::new();
4103 registry.register(FilterTestCapability { priority: 0 });
4104
4105 let configs = vec![AgentCapabilityConfig {
4106 capability_ref: CapabilityId::new("filter_test"),
4107 config: serde_json::json!({}),
4108 }];
4109
4110 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4111 assert!(
4112 collected
4113 .mounts
4114 .iter()
4115 .all(|m| !m.path.starts_with("/.agents/skills/"))
4116 );
4117 }
4118}