1use crate::capability_types::is_plugin_capability;
22use crate::command::{
23 CommandDescriptor, CommandExecutionContext, CommandResult, ExecuteCommandRequest,
24};
25use crate::deployment::DeploymentGrade;
26use crate::events::TokenUsage;
27use crate::mcp_server::{ScopedMcpServers, merge_scoped_mcp_servers};
28use crate::message::Message;
29use crate::message_filter::MessageFilterProvider;
30use crate::runtime_agent::RuntimeAgent;
31use crate::tool_types::{ToolCall, ToolDefinition};
32use crate::tools::{Tool, ToolRegistry};
33use crate::traits::SessionFileSystem;
34use crate::typed_id::SessionId;
35use async_trait::async_trait;
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::sync::Arc;
39
40pub struct IntegrationPlugin {
64 pub experimental_only: bool,
66 pub feature_flag: Option<&'static str>,
69 pub factory: fn() -> Box<dyn Capability>,
71}
72
73inventory::collect!(IntegrationPlugin);
74
75pub use crate::capability_types::{
77 AgentCapabilityConfig, CapabilityId, CapabilityStatus, MountAccess, MountDirectoryBuilder,
78 MountEntry, MountPoint, MountSource,
79};
80
81mod a2a_delegation;
86#[cfg(feature = "ui-capabilities")]
87mod a2ui;
88mod agent_handoff;
89mod agent_instructions;
90pub mod attach_skill;
91mod auto_tool_search;
92mod background_execution;
93mod bashkit_shell;
94mod btw;
95mod budgeting;
96mod claude_tool_search;
97pub mod compaction;
98mod current_time;
99mod data_knowledge;
100mod declarative;
101mod error_disclosure;
102mod fake_aws;
103mod fake_crm;
104mod fake_financial;
105mod fake_warehouse;
106mod file_system;
107mod guardrails;
108mod human_intent;
109mod infinity_context;
110mod knowledge_base;
111mod loop_detection;
112mod lua;
113mod lua_code_mode;
114pub mod mcp;
115mod memory;
116mod message_metadata;
117mod model_scout;
118mod monitors;
119mod noop;
120mod openai_tool_search;
121mod openrouter_workspace;
122#[cfg(feature = "ui-capabilities")]
123mod openui;
124mod platform_management;
125mod prompt_caching;
126mod prompt_canary_guardrail;
127mod research;
128mod sample_data;
129mod self_budget;
130mod session;
131mod session_sandbox;
132mod session_schedule;
133mod session_sql_database;
134mod session_storage;
135mod session_tasks;
136mod skills;
137mod skills_scoped;
138mod stateless_todo_list;
139mod subagents;
140mod system_commands;
141mod test_math;
142mod test_weather;
143mod tool_output_distillation;
144mod tool_output_persistence;
145mod tool_search;
146pub mod user_hooks;
147mod web_fetch;
148
149pub use a2a_delegation::{
151 A2A_AGENT_DELEGATION_CAPABILITY_ID, A2aAgentDelegationCapability, SpawnAgentTool,
152};
153#[cfg(feature = "ui-capabilities")]
154pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
155pub use agent_handoff::{
156 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
157 MessageAgentHandoffTool, StartAgentHandoffTool,
158};
159pub use agent_instructions::{
160 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
161 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
162 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
163};
164pub use attach_skill::{
165 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
166 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
167 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
168};
169pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
170pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
171pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
172pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
173pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
174pub use compaction::{
175 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
176 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
177 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
178 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
179 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
180 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
181 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
182};
183pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
184pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
185pub use declarative::{
186 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
187 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
188 declarative_capability_info, hydrate_declarative_capability_config,
189 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
190 plugin_capability_info, validate_declarative_capability_definition,
191};
192pub use error_disclosure::{
193 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
194};
195pub use fake_aws::{
196 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
197 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
198 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
199 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
200};
201pub use fake_crm::{
202 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
203 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
204 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
205};
206pub use fake_financial::{
207 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
208 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
209 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
210 FinanceListTransactionsTool,
211};
212pub use fake_warehouse::{
213 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
214 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
215 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
216 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
217};
218pub use file_system::{
219 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
220 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
221};
222pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
223pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
224pub use infinity_context::{
225 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
226};
227pub use knowledge_base::{
228 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
229 validate_knowledge_base_config,
230};
231pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
232pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
233pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
234pub use mcp::{
235 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
236 parse_mcp_capability_id,
237};
238pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
239pub use message_metadata::{
240 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
241 MessageMetadataField, render_annotation,
242};
243pub use model_scout::{
244 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
245 RouterUpdateProposal, compute_score, rank_results,
246};
247pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
248pub use openai_tool_search::{
249 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
250 model_supports_native_tool_search,
251};
252pub use openrouter_workspace::{
253 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
254 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
255 detect_policy_drift,
256};
257#[cfg(feature = "ui-capabilities")]
258pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
259pub use platform_management::{
260 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
261 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
262 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
263};
264pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
265pub use prompt_canary_guardrail::{
266 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
267 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
268 REASON_CODE_SYSTEM_PROMPT_LEAK,
269};
270pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
271pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
272pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
273pub use session::{
274 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
275};
276pub use session_sandbox::{
277 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
278 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
279};
280pub use session_schedule::{
281 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
282 SessionScheduleCapability,
283};
284pub use session_sql_database::{
285 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
286 SqlSchemaTool,
287};
288pub use session_storage::{
289 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
290 is_internal_session_kv_key,
291};
292pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
293pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
294pub use skills_scoped::{
295 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
296};
297pub use stateless_todo_list::{
298 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
299};
300pub use subagents::{SUBAGENTS_CAPABILITY_ID, SubagentCapability};
301pub use bashkit_shell::{
303 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
304};
305pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
306pub use test_math::{
307 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
308};
309pub use test_weather::{
310 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
311};
312pub use tool_output_distillation::{
313 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
314};
315pub use tool_output_persistence::{
316 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
317};
318pub use tool_search::{
319 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
320};
321pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
322pub use web_fetch::{
323 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
324 derive_bot_auth_public_key,
325};
326
327pub struct SystemPromptContext {
337 pub session_id: SessionId,
339 pub locale: Option<String>,
341 pub file_store: Option<Arc<dyn SessionFileSystem>>,
343 pub model: Option<String>,
349}
350
351impl SystemPromptContext {
352 pub fn without_file_store(session_id: SessionId) -> Self {
354 Self {
355 session_id,
356 locale: None,
357 file_store: None,
358 model: None,
359 }
360 }
361
362 pub fn with_model(mut self, model: impl Into<String>) -> Self {
364 self.model = Some(model.into());
365 self
366 }
367}
368
369#[derive(Debug, Clone)]
421pub struct CapabilityLocalization {
422 pub locale: &'static str,
424 pub name: Option<&'static str>,
426 pub description: Option<&'static str>,
428 pub config_description: Option<&'static str>,
433 pub config_overlay: Option<serde_json::Value>,
439}
440
441impl CapabilityLocalization {
442 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
444 Self {
445 locale,
446 name: Some(name),
447 description: Some(description),
448 config_description: None,
449 config_overlay: None,
450 }
451 }
452}
453
454pub fn resolve_localized_field<T>(
458 localizations: &[CapabilityLocalization],
459 locale: Option<&str>,
460 field: impl Fn(&CapabilityLocalization) -> Option<T>,
461) -> Option<T> {
462 let mut candidates: Vec<String> = Vec::new();
463 if let Some(raw) = locale {
464 let normalized = raw.trim().replace('_', "-").to_lowercase();
465 if !normalized.is_empty() {
466 if let Some((language, _)) = normalized.split_once('-') {
467 let language = language.to_string();
468 candidates.push(normalized);
469 candidates.push(language);
470 } else {
471 candidates.push(normalized);
472 }
473 }
474 }
475 candidates.push("en".to_string());
476
477 for candidate in candidates {
478 let hit = localizations
479 .iter()
480 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
481 .and_then(&field);
482 if hit.is_some() {
483 return hit;
484 }
485 }
486 None
487}
488
489#[async_trait]
490pub trait Capability: Send + Sync {
491 fn id(&self) -> &str;
493
494 fn aliases(&self) -> Vec<&'static str> {
503 vec![]
504 }
505
506 fn name(&self) -> &str;
508
509 fn description(&self) -> &str;
511
512 fn localizations(&self) -> Vec<CapabilityLocalization> {
517 vec![]
518 }
519
520 fn localized_name(&self, locale: Option<&str>) -> String {
523 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
524 .unwrap_or_else(|| self.name())
525 .to_string()
526 }
527
528 fn localized_description(&self, locale: Option<&str>) -> String {
530 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
531 .unwrap_or_else(|| self.description())
532 .to_string()
533 }
534
535 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
539 resolve_localized_field(&self.localizations(), locale, |entry| {
540 entry.config_description
541 })
542 .map(str::to_string)
543 }
544
545 fn status(&self) -> CapabilityStatus {
547 CapabilityStatus::Available
548 }
549
550 fn icon(&self) -> Option<&str> {
552 None
553 }
554
555 fn category(&self) -> Option<&str> {
557 None
558 }
559
560 fn is_guardrail(&self) -> bool {
565 false
566 }
567
568 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
579 None
580 }
581
582 fn system_prompt_addition(&self) -> Option<&str> {
602 None
603 }
604
605 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
617 self.system_prompt_addition().map(|addition| {
618 format!(
619 "<capability id=\"{}\">\n{}\n</capability>",
620 self.id(),
621 addition
622 )
623 })
624 }
625
626 fn system_prompt_preview(&self) -> Option<String> {
632 self.system_prompt_addition().map(|s| s.to_string())
633 }
634
635 fn tools(&self) -> Vec<Box<dyn Tool>> {
637 vec![]
638 }
639
640 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
648 self.tools()
649 }
650
651 async fn system_prompt_contribution_with_config(
658 &self,
659 ctx: &SystemPromptContext,
660 _config: &serde_json::Value,
661 ) -> Option<String> {
662 self.system_prompt_contribution(ctx).await
663 }
664
665 fn tool_definitions(&self) -> Vec<ToolDefinition> {
668 self.tools().iter().map(|t| t.to_definition()).collect()
669 }
670
671 fn mounts(&self) -> Vec<MountPoint> {
679 vec![]
680 }
681
682 fn dependencies(&self) -> Vec<&'static str> {
691 vec![]
692 }
693
694 fn features(&self) -> Vec<&'static str> {
709 vec![]
710 }
711
712 fn config_schema(&self) -> Option<serde_json::Value> {
718 None
719 }
720
721 fn config_ui_schema(&self) -> Option<serde_json::Value> {
726 None
727 }
728
729 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
735 Ok(())
736 }
737
738 fn mcp_servers(&self) -> ScopedMcpServers {
744 ScopedMcpServers::default()
745 }
746
747 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
749 self.mcp_servers()
750 }
751
752 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
765 None
766 }
767
768 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
776 None
777 }
778
779 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
790 vec![]
791 }
792
793 fn pre_tool_use_hooks_with_config(
798 &self,
799 _config: &serde_json::Value,
800 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
801 self.pre_tool_use_hooks()
802 }
803
804 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
812 vec![]
813 }
814
815 fn post_tool_exec_hooks_with_config(
820 &self,
821 _config: &serde_json::Value,
822 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
823 self.post_tool_exec_hooks()
824 }
825
826 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
835 vec![]
836 }
837
838 fn tool_definition_hooks_with_config(
843 &self,
844 _config: &serde_json::Value,
845 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
846 self.tool_definition_hooks()
847 }
848
849 fn tool_definition_hooks_with_context(
859 &self,
860 _ctx: &SystemPromptContext,
861 config: &serde_json::Value,
862 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
863 self.tool_definition_hooks_with_config(config)
864 }
865
866 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
874 vec![]
875 }
876
877 fn narrate(
891 &self,
892 _tool_def: Option<&ToolDefinition>,
893 tool_call: &ToolCall,
894 phase: crate::tool_narration::ToolNarrationPhase,
895 locale: Option<&str>,
896 ) -> Option<String> {
897 self.tools()
898 .iter()
899 .find(|tool| tool.name() == tool_call.name)
900 .and_then(|tool| tool.narrate(tool_call, phase, locale))
901 }
902
903 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
919 vec![]
920 }
921
922 fn user_hooks_with_config(
928 &self,
929 _config: &serde_json::Value,
930 ) -> Vec<crate::user_hook_types::UserHookSpec> {
931 self.user_hooks()
932 }
933
934 fn risk_level(&self) -> RiskLevel {
942 RiskLevel::Low
943 }
944
945 fn commands(&self) -> Vec<CommandDescriptor> {
953 vec![]
954 }
955
956 async fn execute_command(
970 &self,
971 request: &ExecuteCommandRequest,
972 _ctx: &CommandExecutionContext,
973 ) -> crate::error::Result<CommandResult> {
974 Err(crate::error::AgentLoopError::config(format!(
975 "capability {} declared command /{} but does not implement execute_command",
976 self.id(),
977 request.name,
978 )))
979 }
980
981 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
989 vec![]
990 }
991
992 fn contribute_skills(&self) -> Vec<SkillContribution> {
1002 vec![]
1003 }
1004
1005 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1016 vec![]
1017 }
1018}
1019
1020pub trait ToolDefinitionHook: Send + Sync {
1021 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1022
1023 fn applies_with_native_tool_search(&self) -> bool {
1028 true
1029 }
1030}
1031
1032pub trait ToolCallHook: Send + Sync {
1033 fn narration(
1034 &self,
1035 _tool_def: Option<&ToolDefinition>,
1036 _tool_call: &ToolCall,
1037 _phase: crate::tool_narration::ToolNarrationPhase,
1038 _locale: Option<&str>,
1039 ) -> Option<String> {
1040 None
1041 }
1042
1043 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1044 tool_call
1045 }
1046}
1047
1048pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1054
1055impl ToolCallHook for CapabilityNarrationHook {
1056 fn narration(
1057 &self,
1058 tool_def: Option<&ToolDefinition>,
1059 tool_call: &ToolCall,
1060 phase: crate::tool_narration::ToolNarrationPhase,
1061 locale: Option<&str>,
1062 ) -> Option<String> {
1063 self.0.narrate(tool_def, tool_call, phase, locale)
1064 }
1065}
1066
1067#[derive(
1071 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1072)]
1073#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1074#[cfg_attr(feature = "openapi", schema(example = "low"))]
1075#[serde(rename_all = "lowercase")]
1076pub enum RiskLevel {
1077 Low,
1079 Medium,
1081 High,
1083}
1084
1085#[derive(Debug, Clone, Serialize, Deserialize)]
1091#[serde(rename_all = "snake_case")]
1092pub enum BlueprintModel {
1093 Fixed(String),
1095 Default(String),
1097 Inherit,
1099}
1100
1101pub struct AgentBlueprint {
1107 pub id: &'static str,
1109 pub name: &'static str,
1111 pub description: &'static str,
1113 pub model: BlueprintModel,
1115 pub system_prompt: &'static str,
1117 pub tools: Vec<Box<dyn Tool>>,
1119 pub max_turns: Option<usize>,
1121 pub config_schema: Option<serde_json::Value>,
1123}
1124
1125impl AgentBlueprint {
1126 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1128 self.tools.iter().map(|t| t.to_definition()).collect()
1129 }
1130}
1131
1132impl std::fmt::Debug for AgentBlueprint {
1133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134 f.debug_struct("AgentBlueprint")
1135 .field("id", &self.id)
1136 .field("name", &self.name)
1137 .field("model", &self.model)
1138 .field("tool_count", &self.tools.len())
1139 .field("max_turns", &self.max_turns)
1140 .finish()
1141 }
1142}
1143
1144#[derive(Clone)]
1171pub struct CapabilityRegistry {
1172 capabilities: HashMap<String, Arc<dyn Capability>>,
1173 aliases: HashMap<String, String>,
1175}
1176
1177impl CapabilityRegistry {
1178 pub fn new() -> Self {
1180 Self {
1181 capabilities: HashMap::new(),
1182 aliases: HashMap::new(),
1183 }
1184 }
1185
1186 pub fn with_builtins() -> Self {
1191 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1192 }
1193
1194 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1199 let mut registry = Self::new();
1200
1201 registry.register(AgentInstructionsCapability);
1203 registry.register(HumanIntentCapability);
1204 registry.register(NoopCapability);
1205 registry.register(CurrentTimeCapability);
1206 registry.register(MessageMetadataCapability);
1207 registry.register(ResearchCapability);
1208 registry.register(ModelScoutCapability);
1209 registry.register(OpenRouterWorkspaceCapability);
1210 registry.register(PlatformManagementCapability);
1211 registry.register(FileSystemCapability);
1212 registry.register(MemoryCapability);
1213 registry.register(SessionStorageCapability);
1214 registry.register(SessionCapability);
1215 registry.register(SessionSqlDatabaseCapability);
1216 registry.register(TestMathCapability);
1217 registry.register(TestWeatherCapability);
1218 registry.register(StatelessTodoListCapability);
1219 registry.register(WebFetchCapability::from_env());
1220 registry.register(BashkitShellCapability);
1221 registry.register(BackgroundExecutionCapability);
1222 registry.register(SessionScheduleCapability);
1223 registry.register(BtwCapability);
1224 registry.register(InfinityContextCapability);
1225 registry.register(budgeting::BudgetingCapability);
1226 registry.register(SelfBudgetCapability);
1227 registry.register(CompactionCapability);
1228 registry.register(ErrorDisclosureCapability);
1229
1230 registry.register(OpenAiToolSearchCapability::new());
1232 registry.register(ClaudeToolSearchCapability::new());
1234 registry.register(ToolSearchCapability::new());
1236 registry.register(AutoToolSearchCapability::new());
1238 registry.register(PromptCachingCapability::new());
1239
1240 registry.register(SkillsCapability);
1242
1243 registry.register(SubagentCapability);
1245
1246 registry.register(SessionTasksCapability);
1248
1249 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1253 registry.register(AgentHandoffCapability);
1254 registry.register(A2aAgentDelegationCapability);
1255 }
1256
1257 registry.register(SystemCommandsCapability);
1259
1260 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1262 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1263
1264 registry.register(user_hooks::UserHooksCapability);
1267
1268 registry.register(LoopDetectionCapability);
1270
1271 registry.register(PromptCanaryGuardrailCapability);
1274
1275 registry.register(GuardrailsCapability);
1278
1279 #[cfg(feature = "ui-capabilities")]
1281 {
1282 registry.register(OpenUiCapability);
1283 registry.register(A2UiCapability);
1284 }
1285
1286 registry.register(SampleDataCapability);
1288
1289 registry.register(DataKnowledgeCapability);
1291
1292 registry.register(KnowledgeBaseCapability);
1294
1295 registry.register(FakeWarehouseCapability);
1297 registry.register(FakeAwsCapability);
1298 registry.register(FakeCrmCapability);
1299 registry.register(FakeFinancialCapability);
1300
1301 let internal_flags = crate::InternalFeatureFlags::from_env();
1303 if internal_flags.session_sandbox {
1304 registry.register(SessionSandboxCapability);
1305 }
1306
1307 if internal_flags.lua {
1311 registry.register(LuaCapability);
1312 registry.register(LuaCodeModeCapability);
1315 }
1316 for plugin in inventory::iter::<IntegrationPlugin>() {
1317 if (!plugin.experimental_only || grade.experimental_features_enabled())
1318 && plugin
1319 .feature_flag
1320 .is_none_or(|f| internal_flags.is_enabled(f))
1321 {
1322 registry.register_boxed((plugin.factory)());
1323 }
1324 }
1325
1326 registry
1327 }
1328
1329 pub fn register(&mut self, capability: impl Capability + 'static) {
1331 self.register_arc(Arc::new(capability));
1332 }
1333
1334 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1336 self.register_arc(Arc::from(capability));
1337 }
1338
1339 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1341 let canonical = capability.id().to_string();
1342 for alias in capability.aliases() {
1343 self.aliases.insert(alias.to_string(), canonical.clone());
1344 }
1345 self.capabilities.insert(canonical, capability);
1346 }
1347
1348 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1350 self.capabilities
1351 .get(id)
1352 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1353 }
1354
1355 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1360 if self.capabilities.contains_key(id) {
1361 Some(id)
1362 } else {
1363 self.aliases
1364 .get(id)
1365 .filter(|c| self.capabilities.contains_key(*c))
1366 .map(String::as_str)
1367 }
1368 }
1369
1370 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1372 let canonical = self.canonical_id(id)?.to_string();
1373 let removed = self.capabilities.remove(&canonical);
1374 self.aliases.retain(|_, target| *target != canonical);
1375 removed
1376 }
1377
1378 pub fn has(&self, id: &str) -> bool {
1380 self.get(id).is_some()
1381 }
1382
1383 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1385 self.capabilities.values().collect()
1386 }
1387
1388 pub fn len(&self) -> usize {
1390 self.capabilities.len()
1391 }
1392
1393 pub fn is_empty(&self) -> bool {
1395 self.capabilities.is_empty()
1396 }
1397
1398 pub fn builder() -> CapabilityRegistryBuilder {
1400 CapabilityRegistryBuilder::new()
1401 }
1402
1403 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1407 for cap in self.capabilities.values() {
1408 for bp in cap.agent_blueprints() {
1409 if bp.id == id {
1410 return Some(bp);
1411 }
1412 }
1413 }
1414 None
1415 }
1416
1417 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1421 for (capability_id, cap) in &self.capabilities {
1422 for bp in cap.agent_blueprints() {
1423 if bp.id == id {
1424 return Some((capability_id.clone(), bp));
1425 }
1426 }
1427 }
1428 None
1429 }
1430
1431 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1433 self.capabilities
1434 .values()
1435 .flat_map(|cap| cap.agent_blueprints())
1436 .collect()
1437 }
1438}
1439
1440impl Default for CapabilityRegistry {
1441 fn default() -> Self {
1442 Self::with_builtins()
1443 }
1444}
1445
1446impl std::fmt::Debug for CapabilityRegistry {
1447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1448 let ids: Vec<_> = self.capabilities.keys().collect();
1449 f.debug_struct("CapabilityRegistry")
1450 .field("capabilities", &ids)
1451 .finish()
1452 }
1453}
1454
1455pub struct CapabilityRegistryBuilder {
1457 registry: CapabilityRegistry,
1458}
1459
1460impl CapabilityRegistryBuilder {
1461 pub fn new() -> Self {
1463 Self {
1464 registry: CapabilityRegistry::new(),
1465 }
1466 }
1467
1468 pub fn with_builtins() -> Self {
1470 Self {
1471 registry: CapabilityRegistry::with_builtins(),
1472 }
1473 }
1474
1475 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1477 self.registry.register(capability);
1478 self
1479 }
1480
1481 pub fn build(self) -> CapabilityRegistry {
1483 self.registry
1484 }
1485}
1486
1487impl Default for CapabilityRegistryBuilder {
1488 fn default() -> Self {
1489 Self::new()
1490 }
1491}
1492
1493pub struct ModelViewContext<'a> {
1499 pub session_id: SessionId,
1500 pub prior_usage: Option<&'a TokenUsage>,
1501}
1502
1503pub trait ModelViewProvider: Send + Sync {
1509 fn apply_model_view(
1510 &self,
1511 messages: Vec<Message>,
1512 config: &serde_json::Value,
1513 context: &ModelViewContext<'_>,
1514 ) -> Vec<Message>;
1515
1516 fn priority(&self) -> i32 {
1517 0
1518 }
1519}
1520
1521pub struct CollectedCapabilities {
1526 pub system_prompt_parts: Vec<String>,
1528 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1530 pub tools: Vec<Box<dyn Tool>>,
1532 pub tool_definitions: Vec<ToolDefinition>,
1534 pub mounts: Vec<MountPoint>,
1536 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1538 pub applied_ids: Vec<String>,
1540 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1542 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1544 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1546 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1548 pub mcp_servers: ScopedMcpServers,
1550 }
1556
1557#[derive(Debug, Clone, PartialEq, Eq)]
1558pub struct SystemPromptAttribution {
1559 pub capability_id: String,
1560 pub content: String,
1561}
1562
1563impl CollectedCapabilities {
1564 pub fn system_prompt_prefix(&self) -> Option<String> {
1567 if self.system_prompt_parts.is_empty() {
1568 None
1569 } else {
1570 Some(self.system_prompt_parts.join("\n\n"))
1571 }
1572 }
1573
1574 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1578 for (provider, config) in &self.message_filter_providers {
1580 provider.apply_filters(query, config);
1581 }
1582 }
1583
1584 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1587 for (provider, config) in &self.message_filter_providers {
1588 provider.post_load(messages, config);
1589 }
1590 }
1591
1592 pub fn has_message_filters(&self) -> bool {
1594 !self.message_filter_providers.is_empty()
1595 }
1596}
1597
1598pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1603 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1604 return base_system_prompt.to_string();
1605 };
1606
1607 if base_system_prompt.is_empty() {
1608 return additions.to_string();
1609 }
1610
1611 if base_system_prompt.contains("<system-prompt>") {
1612 format!("{base_system_prompt}\n\n{additions}")
1613 } else {
1614 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1615 }
1616}
1617
1618pub struct CollectedMessageFilters {
1625 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1627}
1628
1629pub struct CollectedModelViewProviders {
1631 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1633}
1634
1635impl CollectedMessageFilters {
1641 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1643 for (provider, config) in &self.message_filter_providers {
1644 provider.apply_filters(query, config);
1645 }
1646 }
1647
1648 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1650 for (provider, config) in &self.message_filter_providers {
1651 provider.post_load(messages, config);
1652 }
1653 }
1654}
1655
1656impl CollectedModelViewProviders {
1657 pub fn apply_model_view(
1659 &self,
1660 mut messages: Vec<Message>,
1661 context: &ModelViewContext<'_>,
1662 ) -> Vec<Message> {
1663 for (provider, config) in &self.model_view_providers {
1664 messages = provider.apply_model_view(messages, config, context);
1665 }
1666 messages
1667 }
1668}
1669
1670fn compaction_is_enabled(
1676 capability_configs: &[AgentCapabilityConfig],
1677 registry: &CapabilityRegistry,
1678) -> bool {
1679 capability_configs.iter().any(|cap_config| {
1680 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
1681 && registry
1682 .get(cap_config.capability_ref.as_str())
1683 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
1684 })
1685}
1686
1687fn message_filter_config_for(
1696 cap_id: &str,
1697 base: &serde_json::Value,
1698 compaction_on: bool,
1699) -> serde_json::Value {
1700 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
1701 return base.clone();
1702 }
1703 let mut config = base.clone();
1704 match config.as_object_mut() {
1705 Some(map) => {
1706 map.insert(
1707 "compaction_active".to_string(),
1708 serde_json::Value::Bool(true),
1709 );
1710 }
1711 None => {
1712 config = serde_json::json!({ "compaction_active": true });
1713 }
1714 }
1715 config
1716}
1717
1718pub fn collect_message_filters_only(
1724 capability_configs: &[AgentCapabilityConfig],
1725 registry: &CapabilityRegistry,
1726) -> CollectedMessageFilters {
1727 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1728 Vec::new();
1729 let compaction_on = compaction_is_enabled(capability_configs, registry);
1730
1731 for cap_config in capability_configs {
1732 let cap_id = cap_config.capability_ref.as_str();
1733 if let Some(capability) = registry.get(cap_id) {
1734 if capability.status() != CapabilityStatus::Available {
1735 continue;
1736 }
1737 let effective: &dyn Capability = capability
1740 .resolve_for_model(None)
1741 .unwrap_or_else(|| capability.as_ref());
1742 if let Some(provider) = effective.message_filter_provider() {
1743 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
1744 message_filter_providers.push((provider, config));
1745 }
1746 }
1747 }
1748
1749 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1750
1751 CollectedMessageFilters {
1752 message_filter_providers,
1753 }
1754}
1755
1756pub fn collect_model_view_providers(
1763 capability_configs: &[AgentCapabilityConfig],
1764 registry: &CapabilityRegistry,
1765 model: Option<&str>,
1766) -> CollectedModelViewProviders {
1767 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1768
1769 for cap_config in capability_configs {
1770 let cap_id = cap_config.capability_ref.as_str();
1771 if let Some(capability) = registry.get(cap_id) {
1772 if capability.status() != CapabilityStatus::Available {
1773 continue;
1774 }
1775 let effective: &dyn Capability = capability
1776 .resolve_for_model(model)
1777 .unwrap_or_else(|| capability.as_ref());
1778 if let Some(provider) = effective.model_view_provider() {
1779 model_view_providers.push((provider, cap_config.config.clone()));
1780 }
1781 }
1782 }
1783
1784 model_view_providers.sort_by_key(|(p, _)| p.priority());
1785
1786 CollectedModelViewProviders {
1787 model_view_providers,
1788 }
1789}
1790
1791pub fn collect_capability_mcp_servers(
1792 capability_configs: &[AgentCapabilityConfig],
1793 registry: &CapabilityRegistry,
1794) -> ScopedMcpServers {
1795 let mut servers = ScopedMcpServers::default();
1796
1797 for cap_config in capability_configs {
1798 let cap_id = cap_config.capability_ref.as_str();
1799 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
1802 if let Ok(definition) =
1803 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1804 {
1805 if definition.status != CapabilityStatus::Available {
1806 continue;
1807 }
1808 if let Some(contributed) = definition.mcp_servers {
1809 servers = merge_scoped_mcp_servers(&servers, &contributed);
1810 }
1811 }
1812 continue;
1813 }
1814 if let Some(capability) = registry.get(cap_id) {
1815 if capability.status() != CapabilityStatus::Available {
1816 continue;
1817 }
1818 servers = merge_scoped_mcp_servers(
1819 &servers,
1820 &capability.mcp_servers_with_config(&cap_config.config),
1821 );
1822 }
1823 }
1824
1825 servers
1826}
1827
1828pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1835
1836#[derive(Debug, Clone, PartialEq, Eq)]
1838pub enum DependencyError {
1839 CircularDependency {
1841 capability_id: String,
1843 chain: Vec<String>,
1845 },
1846 TooManyCapabilities {
1848 count: usize,
1850 max: usize,
1852 },
1853}
1854
1855impl std::fmt::Display for DependencyError {
1856 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1857 match self {
1858 DependencyError::CircularDependency {
1859 capability_id,
1860 chain,
1861 } => {
1862 write!(
1863 f,
1864 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1865 capability_id,
1866 chain.join(" -> "),
1867 capability_id
1868 )
1869 }
1870 DependencyError::TooManyCapabilities { count, max } => {
1871 write!(
1872 f,
1873 "Too many capabilities after resolution: {} (max: {})",
1874 count, max
1875 )
1876 }
1877 }
1878 }
1879}
1880
1881impl std::error::Error for DependencyError {}
1882
1883#[derive(Debug, Clone)]
1885pub struct ResolvedCapabilities {
1886 pub resolved_ids: Vec<String>,
1889 pub added_as_dependencies: Vec<String>,
1891 pub user_selected: Vec<String>,
1893}
1894
1895pub fn resolve_dependencies(
1915 selected_ids: &[String],
1916 registry: &CapabilityRegistry,
1917) -> Result<ResolvedCapabilities, DependencyError> {
1918 use std::collections::HashSet;
1919
1920 let user_selected: HashSet<String> = selected_ids
1922 .iter()
1923 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
1924 .collect();
1925 let mut resolved: Vec<String> = Vec::new();
1926 let mut resolved_set: HashSet<String> = HashSet::new();
1927 let mut added_as_dependencies: Vec<String> = Vec::new();
1928
1929 for cap_id in selected_ids {
1931 resolve_single_capability(
1932 cap_id,
1933 registry,
1934 &mut resolved,
1935 &mut resolved_set,
1936 &mut added_as_dependencies,
1937 &user_selected,
1938 &mut Vec::new(), )?;
1940 }
1941
1942 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
1944 return Err(DependencyError::TooManyCapabilities {
1945 count: resolved.len(),
1946 max: MAX_RESOLVED_CAPABILITIES,
1947 });
1948 }
1949
1950 Ok(ResolvedCapabilities {
1951 resolved_ids: resolved,
1952 added_as_dependencies,
1953 user_selected: selected_ids.to_vec(),
1954 })
1955}
1956
1957pub fn resolve_capability_configs(
1962 selected_configs: &[AgentCapabilityConfig],
1963 registry: &CapabilityRegistry,
1964) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
1965 let mut selected_ids: Vec<String> = Vec::new();
1966 for config in selected_configs {
1967 if (is_declarative_capability(config.capability_id())
1970 || is_plugin_capability(config.capability_id()))
1971 && let Ok(definition) =
1972 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
1973 {
1974 selected_ids.extend(definition.dependencies);
1975 }
1976 selected_ids.push(config.capability_id().to_string());
1977 }
1978 let resolved = resolve_dependencies(&selected_ids, registry)?;
1979
1980 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
1983 .iter()
1984 .map(|config| {
1985 let id = config.capability_id();
1986 let id = registry.canonical_id(id).unwrap_or(id);
1987 (id.to_string(), config.config.clone())
1988 })
1989 .collect();
1990
1991 Ok(resolved
1992 .resolved_ids
1993 .into_iter()
1994 .map(|capability_id| {
1995 explicit_configs
1996 .get(&capability_id)
1997 .cloned()
1998 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
1999 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2000 })
2001 .collect())
2002}
2003
2004fn resolve_single_capability(
2006 cap_id: &str,
2007 registry: &CapabilityRegistry,
2008 resolved: &mut Vec<String>,
2009 resolved_set: &mut std::collections::HashSet<String>,
2010 added_as_dependencies: &mut Vec<String>,
2011 user_selected: &std::collections::HashSet<String>,
2012 visiting: &mut Vec<String>,
2013) -> Result<(), DependencyError> {
2014 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2018
2019 if resolved_set.contains(cap_id) {
2021 return Ok(());
2022 }
2023
2024 if visiting.contains(&cap_id.to_string()) {
2026 return Err(DependencyError::CircularDependency {
2027 capability_id: cap_id.to_string(),
2028 chain: visiting.clone(),
2029 });
2030 }
2031
2032 let capability = match registry.get(cap_id) {
2034 Some(cap) => cap,
2035 None => {
2036 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2040 && !resolved_set.contains(cap_id)
2041 {
2042 resolved.push(cap_id.to_string());
2043 resolved_set.insert(cap_id.to_string());
2044 if !user_selected.contains(cap_id) {
2045 added_as_dependencies.push(cap_id.to_string());
2046 }
2047 }
2048 return Ok(());
2049 }
2050 };
2051
2052 visiting.push(cap_id.to_string());
2054
2055 for dep_id in capability.dependencies() {
2057 resolve_single_capability(
2058 dep_id,
2059 registry,
2060 resolved,
2061 resolved_set,
2062 added_as_dependencies,
2063 user_selected,
2064 visiting,
2065 )?;
2066 }
2067
2068 visiting.pop();
2070
2071 if !resolved_set.contains(cap_id) {
2073 resolved.push(cap_id.to_string());
2074 resolved_set.insert(cap_id.to_string());
2075
2076 if !user_selected.contains(cap_id) {
2078 added_as_dependencies.push(cap_id.to_string());
2079 }
2080 }
2081
2082 Ok(())
2083}
2084
2085pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2090 use std::collections::HashSet;
2091
2092 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2093 Ok(resolved) => resolved.resolved_ids,
2094 Err(_) => capability_ids.to_vec(),
2095 };
2096
2097 let mut seen = HashSet::new();
2098 let mut features = Vec::new();
2099 for cap_id in &resolved_ids {
2100 if let Some(cap) = registry.get(cap_id) {
2101 for feature in cap.features() {
2102 if seen.insert(feature) {
2103 features.push(feature.to_string());
2104 }
2105 }
2106 }
2107 }
2108 features
2109}
2110
2111pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2114 registry
2115 .get(cap_id)
2116 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2117 .unwrap_or_default()
2118}
2119
2120pub async fn collect_capabilities(
2136 capability_ids: &[String],
2137 registry: &CapabilityRegistry,
2138 ctx: &SystemPromptContext,
2139) -> CollectedCapabilities {
2140 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2143 Ok(resolved) => resolved.resolved_ids,
2144 Err(e) => {
2145 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2146 capability_ids.to_vec()
2147 }
2148 };
2149
2150 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2152 .iter()
2153 .map(|id| AgentCapabilityConfig {
2154 capability_ref: CapabilityId::new(id),
2155 config: serde_json::Value::Object(serde_json::Map::new()),
2156 })
2157 .collect();
2158
2159 collect_capabilities_with_configs(&configs, registry, ctx).await
2160}
2161
2162pub async fn collect_capabilities_with_configs(
2173 capability_configs: &[AgentCapabilityConfig],
2174 registry: &CapabilityRegistry,
2175 ctx: &SystemPromptContext,
2176) -> CollectedCapabilities {
2177 let mut system_prompt_parts: Vec<String> = Vec::new();
2178 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2179 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2180 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2181 let mut mounts: Vec<MountPoint> = Vec::new();
2182 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2183 Vec::new();
2184 let mut applied_ids: Vec<String> = Vec::new();
2185 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2186 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2187 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2188 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2189 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2192 let mut mcp_servers = ScopedMcpServers::default();
2193 let compaction_on = compaction_is_enabled(capability_configs, registry);
2194
2195 for cap_config in capability_configs {
2196 let cap_id = cap_config.capability_ref.as_str();
2197 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2202 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2203 cap_config.config.clone(),
2204 ) {
2205 Ok(definition) => {
2206 if definition.status != CapabilityStatus::Available {
2207 continue;
2208 }
2209
2210 if let Some(prompt) = definition.system_prompt.as_deref() {
2211 let contribution =
2212 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2213 system_prompt_attributions.push(SystemPromptAttribution {
2214 capability_id: cap_id.to_string(),
2215 content: contribution.clone(),
2216 });
2217 system_prompt_parts.push(contribution);
2218 }
2219
2220 mounts.extend(definition.mounts(cap_id));
2221 if let Some(ref servers) = definition.mcp_servers {
2222 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2223 }
2224 for skill in definition.skill_contributions() {
2225 mounts.push(skill.to_mount(cap_id));
2226 }
2227
2228 applied_ids.push(cap_id.to_string());
2229 }
2230 Err(error) => {
2231 tracing::warn!(
2232 capability_id = %cap_id,
2233 error = %error,
2234 "Skipping invalid declarative/plugin capability config"
2235 );
2236 }
2237 }
2238 continue;
2239 }
2240 if let Some(capability) = registry.get(cap_id) {
2241 if capability.status() != CapabilityStatus::Available {
2243 continue;
2244 }
2245
2246 let effective: &dyn Capability =
2258 match capability.resolve_for_model(ctx.model.as_deref()) {
2259 Some(inner) => inner,
2260 None => capability.as_ref(),
2261 };
2262 let effective_id = effective.id();
2263
2264 if let Some(contribution) = effective
2266 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2267 .await
2268 {
2269 system_prompt_attributions.push(SystemPromptAttribution {
2270 capability_id: cap_id.to_string(),
2271 content: contribution.clone(),
2272 });
2273 system_prompt_parts.push(contribution);
2274 }
2275
2276 tools.extend(effective.tools_with_config(&cap_config.config));
2278 tool_definition_hooks
2279 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2280 tool_call_hooks.extend(effective.tool_call_hooks());
2281 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2283 let cap_category = effective.category();
2288 for def in effective.tool_definitions() {
2289 let def = match (def.category(), cap_category) {
2290 (None, Some(cat)) => def.with_category(cat),
2291 _ => def,
2292 }
2293 .with_capability_attribution(cap_id, Some(capability.name()));
2294 tool_definitions.push(def);
2295 }
2296
2297 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2305 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2306 {
2307 let threshold = cap_config
2309 .config
2310 .get("threshold")
2311 .and_then(|v| v.as_u64())
2312 .map(|v| v as usize)
2313 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2314 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2315 enabled: true,
2316 threshold,
2317 });
2318 }
2319
2320 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2321 let strategy = cap_config
2322 .config
2323 .get("strategy")
2324 .and_then(|v| v.as_str())
2325 .map(|value| match value {
2326 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2327 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2328 })
2329 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2330 let gemini_cached_content = cap_config
2331 .config
2332 .get("gemini_cached_content")
2333 .and_then(|v| v.as_str())
2334 .map(str::to_string);
2335 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2336 enabled: true,
2337 strategy,
2338 gemini_cached_content,
2339 });
2340 }
2341
2342 mounts.extend(effective.mounts());
2344
2345 mcp_servers = merge_scoped_mcp_servers(
2346 &mcp_servers,
2347 &effective.mcp_servers_with_config(&cap_config.config),
2348 );
2349
2350 for skill in effective.contribute_skills() {
2354 mounts.push(skill.to_mount(cap_id));
2355 }
2356
2357 if let Some(provider) = effective.message_filter_provider() {
2359 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2360 message_filter_providers.push((provider, config));
2361 }
2362
2363 applied_ids.push(cap_id.to_string());
2364 }
2365 }
2366
2367 if !applied_ids
2379 .iter()
2380 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2381 && tool_definitions
2382 .iter()
2383 .any(|def| def.hints().supports_background == Some(true))
2384 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2385 && bg_cap.status() == CapabilityStatus::Available
2386 {
2387 tools.extend(bg_cap.tools());
2388 let cap_category = bg_cap.category();
2389 for def in bg_cap.tool_definitions() {
2390 let def = match (def.category(), cap_category) {
2391 (None, Some(cat)) => def.with_category(cat),
2392 _ => def,
2393 }
2394 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2395 tool_definitions.push(def);
2396 }
2397 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2398 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2399 }
2400
2401 tool_call_hooks.extend(narration_hooks);
2405
2406 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2408
2409 CollectedCapabilities {
2410 system_prompt_parts,
2411 system_prompt_attributions,
2412 tools,
2413 tool_definitions,
2414 mounts,
2415 message_filter_providers,
2416 applied_ids,
2417 tool_search,
2418 prompt_cache,
2419 tool_definition_hooks,
2420 tool_call_hooks,
2421 mcp_servers,
2422 }
2423}
2424
2425pub struct AppliedCapabilities {
2431 pub runtime_agent: RuntimeAgent,
2433 pub tool_registry: ToolRegistry,
2435 pub applied_ids: Vec<String>,
2437}
2438
2439pub async fn apply_capabilities(
2476 base_runtime_agent: RuntimeAgent,
2477 capability_ids: &[String],
2478 registry: &CapabilityRegistry,
2479 ctx: &SystemPromptContext,
2480) -> AppliedCapabilities {
2481 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2482
2483 let final_system_prompt = compose_system_prompt(
2485 &base_runtime_agent.system_prompt,
2486 collected.system_prompt_prefix().as_deref(),
2487 );
2488
2489 let mut tool_registry = ToolRegistry::new();
2491 for tool in collected.tools {
2492 tool_registry.register_boxed(tool);
2493 }
2494
2495 let mut tools = collected.tool_definitions;
2497 for hook in &collected.tool_definition_hooks {
2498 tools = hook.transform(tools);
2499 }
2500
2501 let runtime_agent = RuntimeAgent {
2502 system_prompt: final_system_prompt,
2503 model: base_runtime_agent.model,
2504 tools,
2505 max_iterations: base_runtime_agent.max_iterations,
2506 temperature: base_runtime_agent.temperature,
2507 max_tokens: base_runtime_agent.max_tokens,
2508 tool_search: collected.tool_search,
2509 prompt_cache: collected.prompt_cache,
2510 network_access: base_runtime_agent.network_access,
2511 };
2512
2513 AppliedCapabilities {
2514 runtime_agent,
2515 tool_registry,
2516 applied_ids: collected.applied_ids,
2517 }
2518}
2519
2520#[cfg(test)]
2525mod tests {
2526 use super::*;
2527 use crate::typed_id::SessionId;
2528 use std::collections::BTreeSet;
2529 use uuid::Uuid;
2530
2531 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2533
2534 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2535 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2536 }
2537
2538 fn test_ctx() -> SystemPromptContext {
2540 SystemPromptContext::without_file_store(SessionId::new())
2541 }
2542
2543 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2545 let mut ids = [
2546 "agent_instructions",
2547 "human_intent",
2548 "budgeting",
2549 "self_budget",
2550 "noop",
2551 "current_time",
2552 "research",
2553 "platform_management",
2554 "session_file_system",
2555 "session_storage",
2556 "session",
2557 "session_sql_database",
2558 "test_math",
2559 "test_weather",
2560 "stateless_todo_list",
2561 "web_fetch",
2562 "bashkit_shell",
2563 "background_execution",
2564 "session_schedule",
2565 "btw",
2566 "infinity_context",
2567 "compaction",
2568 "memory",
2569 "message_metadata",
2570 "openai_tool_search",
2571 "claude_tool_search",
2572 "tool_search",
2573 "auto_tool_search",
2574 "prompt_caching",
2575 "session_tasks",
2576 "skills",
2577 "subagents",
2578 "system_commands",
2579 "sample_data",
2580 "data_knowledge",
2581 "knowledge_base",
2582 "tool_output_persistence",
2583 "tool_output_distillation",
2584 "fake_warehouse",
2585 "fake_aws",
2586 "fake_crm",
2587 "fake_financial",
2588 "loop_detection",
2589 "error_disclosure",
2590 "prompt_canary_guardrail",
2591 "guardrails",
2592 "user_hooks",
2593 "model_scout",
2594 "openrouter_workspace",
2595 ]
2596 .into_iter()
2597 .collect::<BTreeSet<_>>();
2598 if cfg!(feature = "ui-capabilities") {
2599 ids.insert("openui");
2600 ids.insert("a2ui");
2601 }
2602 ids
2603 }
2604
2605 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2607 let mut ids = expected_core_builtin_ids();
2608 ids.insert("agent_handoff");
2609 ids.insert("a2a_agent_delegation");
2610 ids
2611 }
2612
2613 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2614 registry.capabilities.keys().map(String::as_str).collect()
2615 }
2616
2617 #[test]
2627 fn test_capability_registry_with_builtins_dev() {
2628 let _lock = lock_env();
2630 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2631 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2632 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2633 assert!(registry.has("agent_handoff"));
2634 assert!(registry.has("a2a_agent_delegation"));
2635 }
2636
2637 #[test]
2638 fn test_capability_registry_with_builtins_prod() {
2639 let _lock = lock_env();
2641 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2642 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2643 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2644 assert!(!registry.has("docker_container"));
2646 assert!(!registry.has("agent_handoff"));
2647 assert!(!registry.has("a2a_agent_delegation"));
2648 }
2649
2650 #[test]
2651 fn test_agent_delegation_enabled_by_env_in_prod() {
2652 let _lock = lock_env();
2654 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2655 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2656 assert!(registry.has("agent_handoff"));
2657 assert!(registry.has("a2a_agent_delegation"));
2658 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2659 }
2660
2661 #[test]
2662 fn test_agent_delegation_disabled_by_env_in_dev() {
2663 let _lock = lock_env();
2665 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2666 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2667 assert!(!registry.has("agent_handoff"));
2668 assert!(!registry.has("a2a_agent_delegation"));
2669 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2670 }
2671
2672 #[test]
2673 fn test_capability_registry_get() {
2674 let registry = CapabilityRegistry::with_builtins();
2675
2676 let noop = registry.get("noop").unwrap();
2677 assert_eq!(noop.id(), "noop");
2678 assert_eq!(noop.name(), "No-Op");
2679 assert_eq!(noop.status(), CapabilityStatus::Available);
2680 }
2681
2682 #[test]
2683 fn test_capability_registry_blueprint_with_capability() {
2684 struct BlueprintProviderCapability;
2685
2686 impl Capability for BlueprintProviderCapability {
2687 fn id(&self) -> &str {
2688 "blueprint_provider"
2689 }
2690 fn name(&self) -> &str {
2691 "Blueprint Provider"
2692 }
2693 fn description(&self) -> &str {
2694 "Capability that provides a blueprint for tests"
2695 }
2696 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2697 vec![AgentBlueprint {
2698 id: "test_blueprint",
2699 name: "Test Blueprint",
2700 description: "Blueprint for capability registry tests",
2701 model: BlueprintModel::Inherit,
2702 system_prompt: "Test prompt",
2703 tools: vec![],
2704 max_turns: None,
2705 config_schema: None,
2706 }]
2707 }
2708 }
2709
2710 let mut registry = CapabilityRegistry::new();
2711 registry.register(BlueprintProviderCapability);
2712
2713 let (capability_id, blueprint) = registry
2714 .blueprint_with_capability("test_blueprint")
2715 .expect("blueprint should resolve with capability id");
2716 assert_eq!(capability_id, "blueprint_provider");
2717 assert_eq!(blueprint.id, "test_blueprint");
2718 }
2719
2720 #[test]
2721 fn test_capability_registry_builder() {
2722 let registry = CapabilityRegistry::builder()
2723 .capability(NoopCapability)
2724 .capability(CurrentTimeCapability)
2725 .build();
2726
2727 assert!(registry.has("noop"));
2728 assert!(registry.has("current_time"));
2729 assert_eq!(registry.len(), 2);
2730 }
2731
2732 #[test]
2733 fn test_capability_status() {
2734 let registry = CapabilityRegistry::with_builtins();
2735
2736 let current_time = registry.get("current_time").unwrap();
2737 assert_eq!(current_time.status(), CapabilityStatus::Available);
2738
2739 let research = registry.get("research").unwrap();
2740 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2741 }
2742
2743 #[test]
2744 fn test_capability_icons_and_categories() {
2745 let registry = CapabilityRegistry::with_builtins();
2746
2747 let noop = registry.get("noop").unwrap();
2748 assert_eq!(noop.icon(), Some("circle-off"));
2749 assert_eq!(noop.category(), Some("Testing"));
2750
2751 let current_time = registry.get("current_time").unwrap();
2752 assert_eq!(current_time.icon(), Some("clock"));
2753 assert_eq!(current_time.category(), Some("Core"));
2754 }
2755
2756 #[test]
2757 fn test_system_prompt_preview_default_delegates_to_addition() {
2758 let registry = CapabilityRegistry::with_builtins();
2759
2760 let test_math = registry.get("test_math").unwrap();
2762 assert_eq!(
2763 test_math.system_prompt_preview().as_deref(),
2764 test_math.system_prompt_addition()
2765 );
2766
2767 let current_time = registry.get("current_time").unwrap();
2769 assert!(current_time.system_prompt_preview().is_none());
2770 assert!(current_time.system_prompt_addition().is_none());
2771 }
2772
2773 #[test]
2774 fn test_system_prompt_preview_dynamic_capability() {
2775 let registry = CapabilityRegistry::with_builtins();
2776 let cap = registry.get("agent_instructions").unwrap();
2777
2778 assert!(cap.system_prompt_addition().is_none());
2780 assert!(cap.system_prompt_preview().is_some());
2781 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2782 }
2783
2784 #[tokio::test]
2789 async fn test_apply_capabilities_empty() {
2790 let registry = CapabilityRegistry::with_builtins();
2791 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2792
2793 let applied =
2794 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
2795
2796 assert_eq!(
2797 applied.runtime_agent.system_prompt,
2798 base_runtime_agent.system_prompt
2799 );
2800 assert!(applied.tool_registry.is_empty());
2801 assert!(applied.applied_ids.is_empty());
2802 }
2803
2804 #[tokio::test]
2805 async fn test_apply_capabilities_noop() {
2806 let registry = CapabilityRegistry::with_builtins();
2807 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2808
2809 let applied = apply_capabilities(
2810 base_runtime_agent.clone(),
2811 &["noop".to_string()],
2812 ®istry,
2813 &test_ctx(),
2814 )
2815 .await;
2816
2817 assert_eq!(
2819 applied.runtime_agent.system_prompt,
2820 base_runtime_agent.system_prompt
2821 );
2822 assert!(applied.tool_registry.is_empty());
2823 assert_eq!(applied.applied_ids, vec!["noop"]);
2824 }
2825
2826 #[tokio::test]
2827 async fn test_apply_capabilities_current_time() {
2828 let registry = CapabilityRegistry::with_builtins();
2829 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2830
2831 let applied = apply_capabilities(
2832 base_runtime_agent.clone(),
2833 &["current_time".to_string()],
2834 ®istry,
2835 &test_ctx(),
2836 )
2837 .await;
2838
2839 assert_eq!(
2841 applied.runtime_agent.system_prompt,
2842 base_runtime_agent.system_prompt
2843 );
2844 assert!(applied.tool_registry.has("get_current_time"));
2845 assert_eq!(applied.tool_registry.len(), 1);
2846 assert_eq!(applied.applied_ids, vec!["current_time"]);
2847 }
2848
2849 #[tokio::test]
2850 async fn test_apply_capabilities_skips_coming_soon() {
2851 let registry = CapabilityRegistry::with_builtins();
2852 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2853
2854 let applied = apply_capabilities(
2856 base_runtime_agent.clone(),
2857 &["research".to_string()],
2858 ®istry,
2859 &test_ctx(),
2860 )
2861 .await;
2862
2863 assert_eq!(
2865 applied.runtime_agent.system_prompt,
2866 base_runtime_agent.system_prompt
2867 );
2868 assert!(applied.applied_ids.is_empty()); }
2870
2871 #[tokio::test]
2872 async fn test_apply_capabilities_multiple() {
2873 let registry = CapabilityRegistry::with_builtins();
2874 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2875
2876 let applied = apply_capabilities(
2877 base_runtime_agent.clone(),
2878 &["noop".to_string(), "current_time".to_string()],
2879 ®istry,
2880 &test_ctx(),
2881 )
2882 .await;
2883
2884 assert!(applied.tool_registry.has("get_current_time"));
2885 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
2886 }
2887
2888 #[tokio::test]
2889 async fn test_apply_capabilities_preserves_order() {
2890 let registry = CapabilityRegistry::with_builtins();
2891 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
2892
2893 let applied = apply_capabilities(
2895 base_runtime_agent,
2896 &["current_time".to_string(), "noop".to_string()],
2897 ®istry,
2898 &test_ctx(),
2899 )
2900 .await;
2901
2902 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
2903 }
2904
2905 #[tokio::test]
2906 async fn test_apply_capabilities_test_math() {
2907 let registry = CapabilityRegistry::with_builtins();
2908 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2909
2910 let applied = apply_capabilities(
2911 base_runtime_agent.clone(),
2912 &["test_math".to_string()],
2913 ®istry,
2914 &test_ctx(),
2915 )
2916 .await;
2917
2918 assert!(
2920 !applied
2921 .runtime_agent
2922 .system_prompt
2923 .contains("<capability id=\"test_math\">")
2924 );
2925 assert!(
2927 applied
2928 .runtime_agent
2929 .system_prompt
2930 .contains("You are a helpful assistant.")
2931 );
2932 assert!(applied.tool_registry.has("add"));
2933 assert!(applied.tool_registry.has("subtract"));
2934 assert!(applied.tool_registry.has("multiply"));
2935 assert!(applied.tool_registry.has("divide"));
2936 assert_eq!(applied.tool_registry.len(), 4);
2937 }
2938
2939 #[tokio::test]
2940 async fn test_apply_capabilities_test_weather() {
2941 let registry = CapabilityRegistry::with_builtins();
2942 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2943
2944 let applied = apply_capabilities(
2945 base_runtime_agent.clone(),
2946 &["test_weather".to_string()],
2947 ®istry,
2948 &test_ctx(),
2949 )
2950 .await;
2951
2952 assert!(
2954 !applied
2955 .runtime_agent
2956 .system_prompt
2957 .contains("<capability id=\"test_weather\">")
2958 );
2959 assert!(applied.tool_registry.has("get_weather"));
2960 assert!(applied.tool_registry.has("get_forecast"));
2961 assert_eq!(applied.tool_registry.len(), 2);
2962 }
2963
2964 #[tokio::test]
2965 async fn test_apply_capabilities_test_math_and_test_weather() {
2966 let registry = CapabilityRegistry::with_builtins();
2967 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2968
2969 let applied = apply_capabilities(
2970 base_runtime_agent.clone(),
2971 &["test_math".to_string(), "test_weather".to_string()],
2972 ®istry,
2973 &test_ctx(),
2974 )
2975 .await;
2976
2977 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
2980 assert!(applied.tool_registry.has("get_weather"));
2981 }
2982
2983 #[tokio::test]
2984 async fn test_apply_capabilities_stateless_todo_list() {
2985 let registry = CapabilityRegistry::with_builtins();
2986 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2987
2988 let applied = apply_capabilities(
2989 base_runtime_agent.clone(),
2990 &["stateless_todo_list".to_string()],
2991 ®istry,
2992 &test_ctx(),
2993 )
2994 .await;
2995
2996 assert!(
2998 applied
2999 .runtime_agent
3000 .system_prompt
3001 .contains("Task Management")
3002 );
3003 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3004 assert!(applied.tool_registry.has("write_todos"));
3005 assert_eq!(applied.tool_registry.len(), 1);
3006 }
3007
3008 #[tokio::test]
3009 async fn test_apply_capabilities_web_fetch() {
3010 let registry = CapabilityRegistry::with_builtins();
3011 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3012
3013 let applied = apply_capabilities(
3014 base_runtime_agent.clone(),
3015 &["web_fetch".to_string()],
3016 ®istry,
3017 &test_ctx(),
3018 )
3019 .await;
3020
3021 assert!(
3023 applied
3024 .runtime_agent
3025 .system_prompt
3026 .contains(&base_runtime_agent.system_prompt)
3027 );
3028 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3029 assert!(applied.tool_registry.has("web_fetch"));
3030 assert_eq!(applied.tool_registry.len(), 1);
3031 }
3032
3033 #[tokio::test]
3038 async fn test_xml_tags_wrap_capability_prompts() {
3039 let registry = CapabilityRegistry::with_builtins();
3040 let collected =
3041 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3042 .await;
3043
3044 assert_eq!(collected.system_prompt_parts.len(), 1);
3045 let part = &collected.system_prompt_parts[0];
3046 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3047 assert!(part.ends_with("</capability>"));
3048 assert!(part.contains("Task Management"));
3049 }
3050
3051 #[tokio::test]
3052 async fn test_xml_tags_multiple_capabilities() {
3053 let registry = CapabilityRegistry::with_builtins();
3054 let collected = collect_capabilities(
3055 &[
3056 "stateless_todo_list".to_string(),
3057 "session_schedule".to_string(),
3058 ],
3059 ®istry,
3060 &test_ctx(),
3061 )
3062 .await;
3063
3064 assert_eq!(collected.system_prompt_parts.len(), 2);
3065 assert!(
3066 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3067 );
3068 assert!(
3069 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3070 );
3071
3072 let prefix = collected.system_prompt_prefix().unwrap();
3073 assert!(prefix.contains("</capability>\n\n<capability"));
3075 }
3076
3077 #[tokio::test]
3078 async fn test_xml_tags_system_prompt_wrapping() {
3079 let registry = CapabilityRegistry::with_builtins();
3080 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3081
3082 let applied = apply_capabilities(
3083 base,
3084 &["stateless_todo_list".to_string()],
3085 ®istry,
3086 &test_ctx(),
3087 )
3088 .await;
3089
3090 let prompt = &applied.runtime_agent.system_prompt;
3091 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3092 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3094 assert!(prompt.contains("</capability>"));
3095 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3097 }
3098
3099 #[tokio::test]
3100 async fn test_no_xml_wrapping_without_capabilities() {
3101 let registry = CapabilityRegistry::with_builtins();
3102 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3103
3104 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3105
3106 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3108 assert!(
3109 !applied
3110 .runtime_agent
3111 .system_prompt
3112 .contains("<system-prompt>")
3113 );
3114 }
3115
3116 #[tokio::test]
3117 async fn test_no_xml_wrapping_for_noop_capability() {
3118 let registry = CapabilityRegistry::with_builtins();
3119 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3120
3121 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3123
3124 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3125 assert!(
3126 !applied
3127 .runtime_agent
3128 .system_prompt
3129 .contains("<system-prompt>")
3130 );
3131 }
3132
3133 #[tokio::test]
3138 async fn test_collect_capabilities_includes_mounts() {
3139 let registry = CapabilityRegistry::with_builtins();
3140
3141 let collected =
3142 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3143
3144 assert!(!collected.mounts.is_empty());
3145 assert_eq!(collected.mounts.len(), 1);
3146 assert_eq!(collected.mounts[0].path, "/samples");
3147 assert!(collected.mounts[0].is_readonly());
3148 }
3149
3150 #[tokio::test]
3151 async fn test_collect_capabilities_empty_mounts_by_default() {
3152 let registry = CapabilityRegistry::with_builtins();
3153
3154 let collected =
3156 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3157
3158 assert!(collected.mounts.is_empty());
3159 }
3160
3161 #[tokio::test]
3162 async fn test_collect_capabilities_combines_mounts() {
3163 let registry = CapabilityRegistry::with_builtins();
3164
3165 let collected = collect_capabilities(
3168 &["sample_data".to_string(), "current_time".to_string()],
3169 ®istry,
3170 &test_ctx(),
3171 )
3172 .await;
3173
3174 assert_eq!(collected.mounts.len(), 1);
3175 assert!(
3177 collected
3178 .applied_ids
3179 .iter()
3180 .any(|id| id == "session_file_system")
3181 );
3182 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3183 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3184 }
3185
3186 #[test]
3187 fn test_sample_data_capability() {
3188 let registry = CapabilityRegistry::with_builtins();
3189 let cap = registry.get("sample_data").unwrap();
3190
3191 assert_eq!(cap.id(), "sample_data");
3192 assert_eq!(cap.name(), "Sample Data");
3193 assert_eq!(cap.status(), CapabilityStatus::Available);
3194
3195 assert!(cap.system_prompt_addition().is_some());
3197 assert!(cap.tools().is_empty());
3198
3199 assert!(!cap.mounts().is_empty());
3201 }
3202
3203 #[test]
3208 fn test_resolve_dependencies_empty() {
3209 let registry = CapabilityRegistry::with_builtins();
3210
3211 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3212
3213 assert!(resolved.resolved_ids.is_empty());
3214 assert!(resolved.added_as_dependencies.is_empty());
3215 assert!(resolved.user_selected.is_empty());
3216 }
3217
3218 #[test]
3219 fn test_resolve_dependencies_no_deps() {
3220 let registry = CapabilityRegistry::with_builtins();
3221
3222 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3224
3225 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3226 assert!(resolved.added_as_dependencies.is_empty());
3227 }
3228
3229 #[test]
3230 fn test_resolve_dependencies_with_deps() {
3231 let registry = CapabilityRegistry::with_builtins();
3232
3233 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3235
3236 assert_eq!(resolved.resolved_ids.len(), 2);
3238 let fs_pos = resolved
3239 .resolved_ids
3240 .iter()
3241 .position(|id| id == "session_file_system")
3242 .unwrap();
3243 let sd_pos = resolved
3244 .resolved_ids
3245 .iter()
3246 .position(|id| id == "sample_data")
3247 .unwrap();
3248 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3249
3250 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3252 }
3253
3254 #[test]
3255 fn test_resolve_dependencies_already_selected() {
3256 let registry = CapabilityRegistry::with_builtins();
3257
3258 let resolved = resolve_dependencies(
3260 &["session_file_system".to_string(), "sample_data".to_string()],
3261 ®istry,
3262 )
3263 .unwrap();
3264
3265 assert_eq!(resolved.resolved_ids.len(), 2);
3266 assert!(resolved.added_as_dependencies.is_empty());
3268 }
3269
3270 #[test]
3271 fn test_resolve_dependencies_preserves_order() {
3272 let registry = CapabilityRegistry::with_builtins();
3273
3274 let resolved =
3276 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
3277 .unwrap();
3278
3279 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
3280 }
3281
3282 #[test]
3283 fn test_resolve_dependencies_unknown_capability() {
3284 let registry = CapabilityRegistry::with_builtins();
3285
3286 let resolved =
3288 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
3289
3290 assert!(resolved.resolved_ids.is_empty());
3291 }
3292
3293 #[test]
3294 fn test_get_dependencies() {
3295 let registry = CapabilityRegistry::with_builtins();
3296
3297 let deps = get_dependencies("sample_data", ®istry);
3299 assert_eq!(deps, vec!["session_file_system"]);
3300
3301 let deps = get_dependencies("current_time", ®istry);
3303 assert!(deps.is_empty());
3304
3305 let deps = get_dependencies("unknown", ®istry);
3307 assert!(deps.is_empty());
3308 }
3309
3310 #[test]
3311 fn test_sample_data_has_dependency() {
3312 let registry = CapabilityRegistry::with_builtins();
3313 let cap = registry.get("sample_data").unwrap();
3314
3315 let deps = cap.dependencies();
3316 assert_eq!(deps.len(), 1);
3317 assert_eq!(deps[0], "session_file_system");
3318 }
3319
3320 #[test]
3321 fn test_noop_has_no_dependencies() {
3322 let registry = CapabilityRegistry::with_builtins();
3323 let cap = registry.get("noop").unwrap();
3324
3325 assert!(cap.dependencies().is_empty());
3326 }
3327
3328 #[test]
3332 fn test_circular_dependency_error() {
3333 struct CapA;
3335 struct CapB;
3336
3337 impl Capability for CapA {
3338 fn id(&self) -> &str {
3339 "test_cap_a"
3340 }
3341 fn name(&self) -> &str {
3342 "Test A"
3343 }
3344 fn description(&self) -> &str {
3345 "Test capability A"
3346 }
3347 fn dependencies(&self) -> Vec<&'static str> {
3348 vec!["test_cap_b"]
3349 }
3350 }
3351
3352 impl Capability for CapB {
3353 fn id(&self) -> &str {
3354 "test_cap_b"
3355 }
3356 fn name(&self) -> &str {
3357 "Test B"
3358 }
3359 fn description(&self) -> &str {
3360 "Test capability B"
3361 }
3362 fn dependencies(&self) -> Vec<&'static str> {
3363 vec!["test_cap_a"]
3364 }
3365 }
3366
3367 let mut registry = CapabilityRegistry::new();
3368 registry.register(CapA);
3369 registry.register(CapB);
3370
3371 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
3372
3373 assert!(result.is_err());
3374 match result.unwrap_err() {
3375 DependencyError::CircularDependency { capability_id, .. } => {
3376 assert_eq!(capability_id, "test_cap_a");
3377 }
3378 _ => panic!("Expected CircularDependency error"),
3379 }
3380 }
3381
3382 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
3387
3388 struct FilterTestCapability {
3390 priority: i32,
3391 }
3392
3393 impl Capability for FilterTestCapability {
3394 fn id(&self) -> &str {
3395 "filter_test"
3396 }
3397 fn name(&self) -> &str {
3398 "Filter Test"
3399 }
3400 fn description(&self) -> &str {
3401 "Test capability with message filter"
3402 }
3403 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3404 Some(Arc::new(FilterTestProvider {
3405 priority: self.priority,
3406 }))
3407 }
3408 }
3409
3410 struct FilterTestProvider {
3411 priority: i32,
3412 }
3413
3414 impl MessageFilterProvider for FilterTestProvider {
3415 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
3416 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
3418 query
3419 .filters
3420 .push(MessageFilter::Search(search.to_string()));
3421 }
3422 }
3423
3424 fn priority(&self) -> i32 {
3425 self.priority
3426 }
3427 }
3428
3429 #[tokio::test]
3430 async fn test_collect_capabilities_with_configs_no_filter_providers() {
3431 let registry = CapabilityRegistry::with_builtins();
3432 let configs = vec![AgentCapabilityConfig {
3433 capability_ref: CapabilityId::new("current_time"),
3434 config: serde_json::json!({}),
3435 }];
3436
3437 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3438
3439 assert!(collected.message_filter_providers.is_empty());
3440 assert!(!collected.has_message_filters());
3441 }
3442
3443 #[tokio::test]
3444 async fn test_collect_capabilities_with_configs_with_filter_provider() {
3445 let mut registry = CapabilityRegistry::new();
3446 registry.register(FilterTestCapability { priority: 0 });
3447
3448 let configs = vec![AgentCapabilityConfig {
3449 capability_ref: CapabilityId::new("filter_test"),
3450 config: serde_json::json!({ "search": "hello" }),
3451 }];
3452
3453 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3454
3455 assert_eq!(collected.message_filter_providers.len(), 1);
3456 assert!(collected.has_message_filters());
3457 }
3458
3459 #[tokio::test]
3460 async fn test_collect_capabilities_with_configs_filter_priority_order() {
3461 struct HighPriorityCapability;
3463 struct LowPriorityCapability;
3464
3465 impl Capability for HighPriorityCapability {
3466 fn id(&self) -> &str {
3467 "high_priority"
3468 }
3469 fn name(&self) -> &str {
3470 "High Priority"
3471 }
3472 fn description(&self) -> &str {
3473 "Test"
3474 }
3475 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3476 Some(Arc::new(FilterTestProvider { priority: 10 }))
3477 }
3478 }
3479
3480 impl Capability for LowPriorityCapability {
3481 fn id(&self) -> &str {
3482 "low_priority"
3483 }
3484 fn name(&self) -> &str {
3485 "Low Priority"
3486 }
3487 fn description(&self) -> &str {
3488 "Test"
3489 }
3490 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3491 Some(Arc::new(FilterTestProvider { priority: -5 }))
3492 }
3493 }
3494
3495 let mut registry = CapabilityRegistry::new();
3496 registry.register(HighPriorityCapability);
3497 registry.register(LowPriorityCapability);
3498
3499 let configs = vec![
3501 AgentCapabilityConfig {
3502 capability_ref: CapabilityId::new("high_priority"),
3503 config: serde_json::json!({}),
3504 },
3505 AgentCapabilityConfig {
3506 capability_ref: CapabilityId::new("low_priority"),
3507 config: serde_json::json!({}),
3508 },
3509 ];
3510
3511 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3512
3513 assert_eq!(collected.message_filter_providers.len(), 2);
3515 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
3516 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
3517 }
3518
3519 #[tokio::test]
3520 async fn test_collected_capabilities_apply_message_filters() {
3521 let mut registry = CapabilityRegistry::new();
3522 registry.register(FilterTestCapability { priority: 0 });
3523
3524 let configs = vec![AgentCapabilityConfig {
3525 capability_ref: CapabilityId::new("filter_test"),
3526 config: serde_json::json!({ "search": "test_query" }),
3527 }];
3528
3529 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3530
3531 let session_id: SessionId = Uuid::now_v7().into();
3533 let mut query = MessageQuery::new(session_id);
3534
3535 collected.apply_message_filters(&mut query);
3536
3537 assert_eq!(query.filters.len(), 1);
3539 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3540 }
3541
3542 #[tokio::test]
3543 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3544 struct SearchCapability {
3545 id: &'static str,
3546 search_term: &'static str,
3547 priority: i32,
3548 }
3549
3550 struct SearchProvider {
3551 search_term: &'static str,
3552 priority: i32,
3553 }
3554
3555 impl MessageFilterProvider for SearchProvider {
3556 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3557 query
3558 .filters
3559 .push(MessageFilter::Search(self.search_term.to_string()));
3560 }
3561
3562 fn priority(&self) -> i32 {
3563 self.priority
3564 }
3565 }
3566
3567 impl Capability for SearchCapability {
3568 fn id(&self) -> &str {
3569 self.id
3570 }
3571 fn name(&self) -> &str {
3572 "Search"
3573 }
3574 fn description(&self) -> &str {
3575 "Test"
3576 }
3577 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3578 Some(Arc::new(SearchProvider {
3579 search_term: self.search_term,
3580 priority: self.priority,
3581 }))
3582 }
3583 }
3584
3585 let mut registry = CapabilityRegistry::new();
3586 registry.register(SearchCapability {
3587 id: "cap_a",
3588 search_term: "alpha",
3589 priority: 5,
3590 });
3591 registry.register(SearchCapability {
3592 id: "cap_b",
3593 search_term: "beta",
3594 priority: 1,
3595 });
3596 registry.register(SearchCapability {
3597 id: "cap_c",
3598 search_term: "gamma",
3599 priority: 10,
3600 });
3601
3602 let configs = vec![
3603 AgentCapabilityConfig {
3604 capability_ref: CapabilityId::new("cap_a"),
3605 config: serde_json::json!({}),
3606 },
3607 AgentCapabilityConfig {
3608 capability_ref: CapabilityId::new("cap_b"),
3609 config: serde_json::json!({}),
3610 },
3611 AgentCapabilityConfig {
3612 capability_ref: CapabilityId::new("cap_c"),
3613 config: serde_json::json!({}),
3614 },
3615 ];
3616
3617 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3618
3619 let session_id: SessionId = Uuid::now_v7().into();
3620 let mut query = MessageQuery::new(session_id);
3621
3622 collected.apply_message_filters(&mut query);
3623
3624 assert_eq!(query.filters.len(), 3);
3626 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3627 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3628 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3629 }
3630
3631 #[test]
3632 fn test_capability_without_message_filter_returns_none() {
3633 let registry = CapabilityRegistry::with_builtins();
3634
3635 let noop = registry.get("noop").unwrap();
3636 assert!(noop.message_filter_provider().is_none());
3637
3638 let current_time = registry.get("current_time").unwrap();
3639 assert!(current_time.message_filter_provider().is_none());
3640 }
3641
3642 #[tokio::test]
3643 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3644 let mut registry = CapabilityRegistry::new();
3645 registry.register(FilterTestCapability { priority: 0 });
3646
3647 let test_config = serde_json::json!({
3648 "search": "custom_search",
3649 "extra_field": 42
3650 });
3651
3652 let configs = vec![AgentCapabilityConfig {
3653 capability_ref: CapabilityId::new("filter_test"),
3654 config: test_config.clone(),
3655 }];
3656
3657 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3658
3659 assert_eq!(collected.message_filter_providers.len(), 1);
3661 let (_, stored_config) = &collected.message_filter_providers[0];
3662 assert_eq!(*stored_config, test_config);
3663 }
3664
3665 #[test]
3670 fn test_collect_message_filters_only_collects_filters() {
3671 let mut registry = CapabilityRegistry::new();
3672 registry.register(FilterTestCapability { priority: 0 });
3673
3674 let configs = vec![AgentCapabilityConfig {
3675 capability_ref: CapabilityId::new("filter_test"),
3676 config: serde_json::json!({ "search": "test_query" }),
3677 }];
3678
3679 let collected = collect_message_filters_only(&configs, ®istry);
3680
3681 let session_id: SessionId = Uuid::now_v7().into();
3682 let mut query = MessageQuery::new(session_id);
3683 collected.apply_message_filters(&mut query);
3684
3685 assert_eq!(query.filters.len(), 1);
3686 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3687 }
3688
3689 #[test]
3690 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
3691 let base = serde_json::json!({ "context_budget_tokens": 1000 });
3692
3693 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
3695 assert_eq!(with["compaction_active"], serde_json::json!(true));
3696 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
3697
3698 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
3699 assert!(without.get("compaction_active").is_none());
3700
3701 let other = message_filter_config_for("other", &base, true);
3703 assert!(other.get("compaction_active").is_none());
3704
3705 let null_base = message_filter_config_for(
3707 INFINITY_CONTEXT_CAPABILITY_ID,
3708 &serde_json::Value::Null,
3709 true,
3710 );
3711 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
3712 }
3713
3714 #[test]
3715 fn test_infinity_context_defers_to_compaction_end_to_end() {
3716 use crate::message::Message;
3717
3718 let mut registry = CapabilityRegistry::new();
3719 registry.register(InfinityContextCapability);
3720 registry.register(CompactionCapability);
3721
3722 let tight = serde_json::json!({
3723 "context_budget_tokens": 1,
3724 "min_recent_messages": 1
3725 });
3726
3727 let solo = vec![AgentCapabilityConfig {
3729 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3730 config: tight.clone(),
3731 }];
3732 let mut messages = vec![
3733 Message::user("task"),
3734 Message::assistant("old ".repeat(400)),
3735 Message::user("recent"),
3736 ];
3737 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
3738 assert!(
3739 messages
3740 .iter()
3741 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
3742 "infinity context alone should trim and notice"
3743 );
3744
3745 let both = vec![
3747 AgentCapabilityConfig {
3748 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3749 config: tight,
3750 },
3751 AgentCapabilityConfig {
3752 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3753 config: serde_json::json!({}),
3754 },
3755 ];
3756 let mut messages = vec![
3757 Message::user("task"),
3758 Message::assistant("old ".repeat(400)),
3759 Message::user("recent"),
3760 ];
3761 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
3762 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
3763 assert!(
3764 messages
3765 .iter()
3766 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
3767 "no hidden-history notice when compaction is the active reducer"
3768 );
3769 }
3770
3771 #[test]
3772 fn test_compaction_is_enabled_detects_compaction() {
3773 let mut registry = CapabilityRegistry::new();
3774 registry.register(CompactionCapability);
3775
3776 let with_compaction = vec![AgentCapabilityConfig {
3777 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3778 config: serde_json::json!({}),
3779 }];
3780 assert!(compaction_is_enabled(&with_compaction, ®istry));
3781
3782 let without = vec![AgentCapabilityConfig {
3783 capability_ref: CapabilityId::new("current_time"),
3784 config: serde_json::json!({}),
3785 }];
3786 assert!(!compaction_is_enabled(&without, ®istry));
3787 }
3788
3789 #[test]
3790 fn test_collect_message_filters_only_skips_unknown_capabilities() {
3791 let registry = CapabilityRegistry::new();
3792
3793 let configs = vec![AgentCapabilityConfig {
3794 capability_ref: CapabilityId::new("nonexistent"),
3795 config: serde_json::json!({}),
3796 }];
3797
3798 let collected = collect_message_filters_only(&configs, ®istry);
3799 assert!(collected.message_filter_providers.is_empty());
3800 }
3801
3802 #[test]
3803 fn test_collect_message_filters_only_preserves_priority_order() {
3804 struct PriorityFilterCap {
3805 id: &'static str,
3806 search_term: &'static str,
3807 priority: i32,
3808 }
3809
3810 struct PriorityFilterProvider {
3811 search_term: &'static str,
3812 priority: i32,
3813 }
3814
3815 impl Capability for PriorityFilterCap {
3816 fn id(&self) -> &str {
3817 self.id
3818 }
3819 fn name(&self) -> &str {
3820 self.id
3821 }
3822 fn description(&self) -> &str {
3823 "priority test"
3824 }
3825 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3826 Some(Arc::new(PriorityFilterProvider {
3827 search_term: self.search_term,
3828 priority: self.priority,
3829 }))
3830 }
3831 }
3832
3833 impl MessageFilterProvider for PriorityFilterProvider {
3834 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3835 query
3836 .filters
3837 .push(MessageFilter::Search(self.search_term.to_string()));
3838 }
3839 fn priority(&self) -> i32 {
3840 self.priority
3841 }
3842 }
3843
3844 let mut registry = CapabilityRegistry::new();
3845 registry.register(PriorityFilterCap {
3846 id: "gamma",
3847 search_term: "gamma",
3848 priority: 10,
3849 });
3850 registry.register(PriorityFilterCap {
3851 id: "alpha",
3852 search_term: "alpha",
3853 priority: 5,
3854 });
3855 registry.register(PriorityFilterCap {
3856 id: "beta",
3857 search_term: "beta",
3858 priority: 1,
3859 });
3860
3861 let configs = vec![
3862 AgentCapabilityConfig {
3863 capability_ref: CapabilityId::new("gamma"),
3864 config: serde_json::json!({}),
3865 },
3866 AgentCapabilityConfig {
3867 capability_ref: CapabilityId::new("alpha"),
3868 config: serde_json::json!({}),
3869 },
3870 AgentCapabilityConfig {
3871 capability_ref: CapabilityId::new("beta"),
3872 config: serde_json::json!({}),
3873 },
3874 ];
3875
3876 let collected = collect_message_filters_only(&configs, ®istry);
3877
3878 let session_id: SessionId = Uuid::now_v7().into();
3879 let mut query = MessageQuery::new(session_id);
3880 collected.apply_message_filters(&mut query);
3881
3882 assert_eq!(query.filters.len(), 3);
3884 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3885 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3886 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3887 }
3888
3889 #[test]
3890 fn test_collect_message_filters_only_post_load_invoked() {
3891 use crate::message::Message;
3892
3893 struct PostLoadCap;
3894 struct PostLoadProvider;
3895
3896 impl Capability for PostLoadCap {
3897 fn id(&self) -> &str {
3898 "post_load_test"
3899 }
3900 fn name(&self) -> &str {
3901 "PostLoad Test"
3902 }
3903 fn description(&self) -> &str {
3904 "test"
3905 }
3906 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3907 Some(Arc::new(PostLoadProvider))
3908 }
3909 }
3910
3911 impl MessageFilterProvider for PostLoadProvider {
3912 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
3913 fn priority(&self) -> i32 {
3914 0
3915 }
3916 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
3917 messages.reverse();
3919 }
3920 }
3921
3922 let mut registry = CapabilityRegistry::new();
3923 registry.register(PostLoadCap);
3924
3925 let configs = vec![AgentCapabilityConfig {
3926 capability_ref: CapabilityId::new("post_load_test"),
3927 config: serde_json::json!({}),
3928 }];
3929
3930 let collected = collect_message_filters_only(&configs, ®istry);
3931
3932 let mut messages = vec![Message::user("first"), Message::user("second")];
3933 collected.apply_post_load_filters(&mut messages);
3934
3935 assert_eq!(messages[0].text(), Some("second"));
3937 assert_eq!(messages[1].text(), Some("first"));
3938 }
3939
3940 #[test]
3941 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
3942 use crate::tool_types::ToolCall;
3943
3944 fn tool_heavy_messages() -> Vec<Message> {
3945 let mut messages = vec![Message::user("inspect files repeatedly")];
3946 for index in 0..9 {
3947 let call_id = format!("call_{index}");
3948 messages.push(Message::assistant_with_tools(
3949 "",
3950 vec![ToolCall {
3951 id: call_id.clone(),
3952 name: "read_file".to_string(),
3953 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
3954 }],
3955 ));
3956 messages.push(Message::tool_result(
3957 call_id,
3958 Some(serde_json::json!({
3959 "path": "/workspace/src/lib.rs",
3960 "content": format!("{}{}", "large file line\n".repeat(1000), index),
3961 "total_lines": 1000,
3962 "lines_shown": {"start": 1, "end": 1000},
3963 "truncated": false
3964 })),
3965 None,
3966 ));
3967 }
3968 messages
3969 }
3970
3971 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
3972 messages[2]
3973 .tool_result_content()
3974 .and_then(|result| result.result.as_ref())
3975 .and_then(|result| result.get("masked"))
3976 .and_then(|masked| masked.as_bool())
3977 .unwrap_or(false)
3978 }
3979
3980 let mut registry = CapabilityRegistry::new();
3981 registry.register(CompactionCapability);
3982 let context = ModelViewContext {
3983 session_id: SessionId::new(),
3984 prior_usage: None,
3985 };
3986
3987 let no_compaction = collect_model_view_providers(&[], ®istry, None);
3988 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
3989 assert!(!first_tool_result_is_masked(&unmasked));
3990
3991 let compaction = collect_model_view_providers(
3992 &[AgentCapabilityConfig {
3993 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3994 config: serde_json::json!({}),
3995 }],
3996 ®istry,
3997 None,
3998 );
3999 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4000 assert!(first_tool_result_is_masked(&masked));
4001 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4002 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4003 }
4004
4005 struct DelegatingFilterCap {
4008 id: &'static str,
4009 inner: std::sync::Arc<InnerFilterCap>,
4010 }
4011 struct InnerFilterCap;
4012
4013 impl Capability for InnerFilterCap {
4014 fn id(&self) -> &str {
4015 "inner_filter"
4016 }
4017 fn name(&self) -> &str {
4018 "Inner Filter"
4019 }
4020 fn description(&self) -> &str {
4021 "inner"
4022 }
4023 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4024 Some(std::sync::Arc::new(SentinelFilter))
4025 }
4026 }
4027 struct SentinelFilter;
4028 impl MessageFilterProvider for SentinelFilter {
4029 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4030 }
4031 impl Capability for DelegatingFilterCap {
4032 fn id(&self) -> &str {
4033 self.id
4034 }
4035 fn name(&self) -> &str {
4036 "Delegating Filter"
4037 }
4038 fn description(&self) -> &str {
4039 "delegating"
4040 }
4041 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4042 None }
4044 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4045 Some(&*self.inner)
4046 }
4047 }
4048
4049 #[test]
4050 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4051 let inner = std::sync::Arc::new(InnerFilterCap);
4052 let outer = DelegatingFilterCap {
4053 id: "delegating_filter",
4054 inner: inner.clone(),
4055 };
4056
4057 let mut registry = CapabilityRegistry::new();
4058 registry.register(outer);
4059
4060 let configs = vec![AgentCapabilityConfig {
4061 capability_ref: CapabilityId::new("delegating_filter"),
4062 config: serde_json::json!({}),
4063 }];
4064
4065 let collected = collect_message_filters_only(&configs, ®istry);
4068 assert_eq!(
4069 collected.message_filter_providers.len(),
4070 1,
4071 "provider from resolved inner capability must be collected"
4072 );
4073 }
4074
4075 struct DelegatingMvpCap {
4076 id: &'static str,
4077 inner: std::sync::Arc<InnerMvpCap>,
4078 }
4079 struct InnerMvpCap;
4080
4081 impl Capability for InnerMvpCap {
4082 fn id(&self) -> &str {
4083 "inner_mvp"
4084 }
4085 fn name(&self) -> &str {
4086 "Inner MVP"
4087 }
4088 fn description(&self) -> &str {
4089 "inner"
4090 }
4091 fn model_view_provider(
4092 &self,
4093 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4094 struct NoopMvp;
4096 impl crate::capabilities::ModelViewProvider for NoopMvp {
4097 fn apply_model_view(
4098 &self,
4099 messages: Vec<Message>,
4100 _config: &serde_json::Value,
4101 _context: &ModelViewContext<'_>,
4102 ) -> Vec<Message> {
4103 messages
4104 }
4105 }
4106 Some(std::sync::Arc::new(NoopMvp))
4107 }
4108 }
4109 impl Capability for DelegatingMvpCap {
4110 fn id(&self) -> &str {
4111 self.id
4112 }
4113 fn name(&self) -> &str {
4114 "Delegating MVP"
4115 }
4116 fn description(&self) -> &str {
4117 "delegating"
4118 }
4119 fn model_view_provider(
4120 &self,
4121 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4122 None }
4124 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4125 Some(&*self.inner)
4126 }
4127 }
4128
4129 #[test]
4130 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4131 let inner = std::sync::Arc::new(InnerMvpCap);
4132 let outer = DelegatingMvpCap {
4133 id: "delegating_mvp",
4134 inner: inner.clone(),
4135 };
4136
4137 let mut registry = CapabilityRegistry::new();
4138 registry.register(outer);
4139
4140 let configs = vec![AgentCapabilityConfig {
4141 capability_ref: CapabilityId::new("delegating_mvp"),
4142 config: serde_json::json!({}),
4143 }];
4144
4145 let collected = collect_model_view_providers(&configs, ®istry, None);
4148 assert_eq!(
4149 collected.model_view_providers.len(),
4150 1,
4151 "provider from resolved inner capability must be collected"
4152 );
4153 }
4154
4155 #[tokio::test]
4165 async fn test_bashkit_shell_capability_produces_bash_tool() {
4166 let registry = CapabilityRegistry::with_builtins();
4167 let collected =
4168 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4169
4170 let tool_names: Vec<&str> = collected
4171 .tool_definitions
4172 .iter()
4173 .map(|t| t.name())
4174 .collect();
4175 assert!(
4176 tool_names.contains(&"bash"),
4177 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4178 tool_names
4179 );
4180 assert!(
4181 !collected.tools.is_empty(),
4182 "bashkit_shell must provide tool implementations"
4183 );
4184 }
4185
4186 #[tokio::test]
4187 async fn test_generic_harness_capability_set_produces_bash_tool() {
4188 let generic_harness_caps = vec![
4191 "session_file_system".to_string(),
4192 "bashkit_shell".to_string(),
4193 "web_fetch".to_string(),
4194 "session_storage".to_string(),
4195 "session".to_string(),
4196 "agent_instructions".to_string(),
4197 "skills".to_string(),
4198 "infinity_context".to_string(),
4199 "auto_tool_search".to_string(),
4200 ];
4201
4202 let registry = CapabilityRegistry::with_builtins();
4203 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4204
4205 let tool_names: Vec<&str> = collected
4206 .tool_definitions
4207 .iter()
4208 .map(|t| t.name())
4209 .collect();
4210 assert!(
4211 tool_names.contains(&"bash"),
4212 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4213 tool_names
4214 );
4215 }
4216
4217 #[tokio::test]
4218 async fn test_collect_capabilities_tool_count_matches_definitions() {
4219 let registry = CapabilityRegistry::with_builtins();
4222 let collected =
4223 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4224
4225 assert_eq!(
4226 collected.tools.len(),
4227 collected.tool_definitions.len(),
4228 "tool implementations ({}) must match tool definitions ({})",
4229 collected.tools.len(),
4230 collected.tool_definitions.len(),
4231 );
4232 }
4233
4234 #[tokio::test]
4238 async fn test_collect_capabilities_resolves_dependencies() {
4239 let registry = CapabilityRegistry::with_builtins();
4242 let collected =
4243 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4244
4245 assert!(
4247 collected
4248 .applied_ids
4249 .iter()
4250 .any(|id| id == "session_file_system"),
4251 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4252 collected.applied_ids
4253 );
4254
4255 let tool_names: Vec<&str> = collected
4256 .tool_definitions
4257 .iter()
4258 .map(|t| t.name())
4259 .collect();
4260
4261 assert!(
4263 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4264 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
4265 tool_names
4266 );
4267
4268 assert_eq!(
4270 collected.tools.len(),
4271 collected.tool_definitions.len(),
4272 "dependency-added tools must have implementations, not just definitions"
4273 );
4274 }
4275
4276 #[test]
4277 fn test_defaults_do_not_include_bash() {
4278 let registry = crate::ToolRegistry::with_defaults();
4281 assert!(
4282 !registry.has("bash"),
4283 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
4284 );
4285 }
4286
4287 #[tokio::test]
4294 async fn test_background_execution_auto_activates_with_bashkit_shell() {
4295 let registry = CapabilityRegistry::with_builtins();
4296 let collected =
4297 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4298
4299 let tool_names: Vec<&str> = collected
4300 .tool_definitions
4301 .iter()
4302 .map(|t| t.name())
4303 .collect();
4304 assert!(
4305 tool_names.contains(&"spawn_background"),
4306 "spawn_background must be auto-activated when bashkit_shell (a \
4307 background-capable tool) is in the agent's capability set; got: {:?}",
4308 tool_names
4309 );
4310 assert!(
4311 collected
4312 .applied_ids
4313 .iter()
4314 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4315 "background_execution must be in applied_ids when auto-activated; \
4316 got: {:?}",
4317 collected.applied_ids
4318 );
4319
4320 assert!(
4322 collected
4323 .tools
4324 .iter()
4325 .any(|t| t.name() == "spawn_background"),
4326 "spawn_background tool implementation must be present alongside the \
4327 definition (lockstep contract)"
4328 );
4329 }
4330
4331 #[tokio::test]
4334 async fn test_background_execution_does_not_auto_activate_without_hint() {
4335 let registry = CapabilityRegistry::with_builtins();
4336 let collected =
4338 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4339
4340 let tool_names: Vec<&str> = collected
4341 .tool_definitions
4342 .iter()
4343 .map(|t| t.name())
4344 .collect();
4345 assert!(
4346 !tool_names.contains(&"spawn_background"),
4347 "spawn_background must NOT be activated without a background-capable \
4348 tool; got: {:?}",
4349 tool_names
4350 );
4351 assert!(
4352 !collected
4353 .applied_ids
4354 .iter()
4355 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4356 "background_execution must not appear in applied_ids when no \
4357 background-capable tool is present; got: {:?}",
4358 collected.applied_ids
4359 );
4360 }
4361
4362 #[tokio::test]
4366 async fn test_background_execution_explicit_selection_is_idempotent() {
4367 let registry = CapabilityRegistry::with_builtins();
4368 let collected = collect_capabilities(
4369 &[
4370 "bashkit_shell".to_string(),
4371 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
4372 ],
4373 ®istry,
4374 &test_ctx(),
4375 )
4376 .await;
4377
4378 let spawn_background_count = collected
4379 .tool_definitions
4380 .iter()
4381 .filter(|t| t.name() == "spawn_background")
4382 .count();
4383 assert_eq!(
4384 spawn_background_count, 1,
4385 "spawn_background must appear exactly once even when \
4386 background_execution is selected explicitly alongside a \
4387 background-capable tool"
4388 );
4389 let applied_count = collected
4390 .applied_ids
4391 .iter()
4392 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
4393 .count();
4394 assert_eq!(
4395 applied_count, 1,
4396 "background_execution must appear exactly once in applied_ids"
4397 );
4398 }
4399
4400 #[test]
4405 fn test_defaults_do_not_include_spawn_background() {
4406 let registry = crate::ToolRegistry::with_defaults();
4407 assert!(
4408 !registry.has("spawn_background"),
4409 "with_defaults() must not include 'spawn_background' — it comes \
4410 from the background_execution capability (EVE-501)"
4411 );
4412 }
4413
4414 #[test]
4419 fn test_capability_features_default_empty() {
4420 let registry = CapabilityRegistry::with_builtins();
4421
4422 let noop = registry.get("noop").unwrap();
4424 assert!(noop.features().is_empty());
4425
4426 let current_time = registry.get("current_time").unwrap();
4427 assert!(current_time.features().is_empty());
4428 }
4429
4430 #[test]
4431 fn test_file_system_capability_features() {
4432 let registry = CapabilityRegistry::with_builtins();
4433
4434 let fs = registry.get("session_file_system").unwrap();
4435 assert_eq!(fs.features(), vec!["file_system"]);
4436 }
4437
4438 #[test]
4439 fn test_bashkit_shell_capability_features() {
4440 let registry = CapabilityRegistry::with_builtins();
4441
4442 let bash = registry.get("bashkit_shell").unwrap();
4443 assert_eq!(bash.features(), vec!["file_system"]);
4444 }
4445
4446 #[test]
4447 fn test_alias_resolves_to_canonical_capability() {
4448 let registry = CapabilityRegistry::with_builtins();
4449
4450 let via_alias = registry.get("virtual_bash").unwrap();
4452 assert_eq!(via_alias.id(), "bashkit_shell");
4453 assert!(registry.has("virtual_bash"));
4454 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
4455 assert_eq!(
4456 registry.canonical_id("bashkit_shell"),
4457 Some("bashkit_shell")
4458 );
4459 assert_eq!(registry.canonical_id("nonexistent"), None);
4460 }
4461
4462 #[test]
4463 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
4464 let registry = CapabilityRegistry::with_builtins();
4465
4466 let resolved = resolve_dependencies(
4469 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
4470 ®istry,
4471 )
4472 .unwrap();
4473 let bash_ids: Vec<_> = resolved
4474 .resolved_ids
4475 .iter()
4476 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
4477 .collect();
4478 assert_eq!(bash_ids, vec!["bashkit_shell"]);
4479 assert!(
4481 !resolved
4482 .added_as_dependencies
4483 .contains(&"bashkit_shell".to_string())
4484 );
4485 }
4486
4487 #[test]
4488 fn test_alias_preserves_explicit_config_in_resolution() {
4489 let registry = CapabilityRegistry::with_builtins();
4490
4491 let configs = vec![AgentCapabilityConfig::with_config(
4492 "virtual_bash".to_string(),
4493 serde_json::json!({"key": "value"}),
4494 )];
4495 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
4496 let bash = resolved
4497 .iter()
4498 .find(|c| c.capability_id() == "bashkit_shell")
4499 .expect("alias must resolve to canonical bashkit_shell config");
4500 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
4501 }
4502
4503 #[test]
4504 fn test_unregister_by_alias_removes_capability_and_aliases() {
4505 let mut registry = CapabilityRegistry::with_builtins();
4506
4507 assert!(registry.unregister("virtual_bash").is_some());
4508 assert!(!registry.has("bashkit_shell"));
4509 assert!(!registry.has("virtual_bash"));
4510 }
4511
4512 #[test]
4513 fn test_session_storage_capability_features() {
4514 let registry = CapabilityRegistry::with_builtins();
4515
4516 let storage = registry.get("session_storage").unwrap();
4517 let features = storage.features();
4518 assert!(features.contains(&"secrets"));
4519 assert!(features.contains(&"key_value"));
4520 }
4521
4522 #[test]
4523 fn test_session_schedule_capability_features() {
4524 let registry = CapabilityRegistry::with_builtins();
4525
4526 let schedule = registry.get("session_schedule").unwrap();
4527 assert_eq!(schedule.features(), vec!["schedules"]);
4528 }
4529
4530 #[test]
4531 fn test_session_sql_database_capability_features() {
4532 let registry = CapabilityRegistry::with_builtins();
4533
4534 let sql = registry.get("session_sql_database").unwrap();
4535 assert_eq!(sql.features(), vec!["sql_database"]);
4536 }
4537
4538 #[test]
4539 fn test_sample_data_capability_features() {
4540 let registry = CapabilityRegistry::with_builtins();
4541
4542 let sample = registry.get("sample_data").unwrap();
4543 assert_eq!(sample.features(), vec!["file_system"]);
4544 }
4545
4546 #[test]
4547 fn test_compute_features_empty() {
4548 let registry = CapabilityRegistry::with_builtins();
4549
4550 let features = compute_features(&[], ®istry);
4551 assert!(features.is_empty());
4552 }
4553
4554 #[test]
4555 fn test_compute_features_single_capability() {
4556 let registry = CapabilityRegistry::with_builtins();
4557
4558 let features = compute_features(&["session_schedule".to_string()], ®istry);
4559 assert_eq!(features, vec!["schedules"]);
4560 }
4561
4562 #[test]
4563 fn test_compute_features_multiple_capabilities() {
4564 let registry = CapabilityRegistry::with_builtins();
4565
4566 let features = compute_features(
4567 &[
4568 "session_file_system".to_string(),
4569 "session_storage".to_string(),
4570 "session_schedule".to_string(),
4571 ],
4572 ®istry,
4573 );
4574 assert!(features.contains(&"file_system".to_string()));
4575 assert!(features.contains(&"secrets".to_string()));
4576 assert!(features.contains(&"key_value".to_string()));
4577 assert!(features.contains(&"schedules".to_string()));
4578 }
4579
4580 #[test]
4581 fn test_compute_features_deduplicates() {
4582 let registry = CapabilityRegistry::with_builtins();
4583
4584 let features = compute_features(
4586 &[
4587 "session_file_system".to_string(),
4588 "bashkit_shell".to_string(),
4589 ],
4590 ®istry,
4591 );
4592 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
4593 assert_eq!(file_system_count, 1, "file_system should appear only once");
4594 }
4595
4596 #[test]
4597 fn test_compute_features_includes_dependency_features() {
4598 let registry = CapabilityRegistry::with_builtins();
4599
4600 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
4602 assert!(features.contains(&"file_system".to_string()));
4603 }
4604
4605 #[test]
4606 fn test_compute_features_generic_harness_set() {
4607 let registry = CapabilityRegistry::with_builtins();
4608
4609 let features = compute_features(
4611 &[
4612 "session_file_system".to_string(),
4613 "bashkit_shell".to_string(),
4614 "session_storage".to_string(),
4615 "session".to_string(),
4616 "session_schedule".to_string(),
4617 ],
4618 ®istry,
4619 );
4620 assert!(features.contains(&"file_system".to_string()));
4621 assert!(features.contains(&"secrets".to_string()));
4622 assert!(features.contains(&"key_value".to_string()));
4623 assert!(features.contains(&"schedules".to_string()));
4624 }
4625
4626 #[test]
4627 fn test_compute_features_unknown_capability_ignored() {
4628 let registry = CapabilityRegistry::with_builtins();
4629
4630 let features = compute_features(
4631 &["unknown_cap".to_string(), "session_schedule".to_string()],
4632 ®istry,
4633 );
4634 assert_eq!(features, vec!["schedules"]);
4635 }
4636
4637 #[test]
4638 fn test_risk_level_ordering() {
4639 assert!(RiskLevel::Low < RiskLevel::Medium);
4640 assert!(RiskLevel::Medium < RiskLevel::High);
4641 }
4642
4643 #[test]
4644 fn test_risk_level_serde_roundtrip() {
4645 let high = RiskLevel::High;
4646 let json = serde_json::to_string(&high).unwrap();
4647 assert_eq!(json, "\"high\"");
4648 let back: RiskLevel = serde_json::from_str(&json).unwrap();
4649 assert_eq!(back, RiskLevel::High);
4650 }
4651
4652 #[test]
4653 fn test_capability_risk_levels() {
4654 let registry = CapabilityRegistry::with_builtins();
4655
4656 let bash = registry.get("bashkit_shell").unwrap();
4658 assert_eq!(bash.risk_level(), RiskLevel::High);
4659
4660 let fetch = registry.get("web_fetch").unwrap();
4662 assert_eq!(fetch.risk_level(), RiskLevel::High);
4663
4664 let noop = registry.get("noop").unwrap();
4666 assert_eq!(noop.risk_level(), RiskLevel::Low);
4667 }
4668
4669 #[tokio::test]
4674 async fn test_apply_capabilities_openai_tool_search() {
4675 let registry = CapabilityRegistry::with_builtins();
4676 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4677
4678 let applied = apply_capabilities(
4679 base_runtime_agent.clone(),
4680 &["openai_tool_search".to_string()],
4681 ®istry,
4682 &test_ctx(),
4683 )
4684 .await;
4685
4686 assert_eq!(
4688 applied.runtime_agent.system_prompt,
4689 base_runtime_agent.system_prompt
4690 );
4691 assert!(applied.tool_registry.is_empty());
4692 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
4693
4694 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4696 assert!(ts.enabled);
4697 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4698 }
4699
4700 #[tokio::test]
4701 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
4702 let registry = CapabilityRegistry::with_builtins();
4703 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4704
4705 let applied = apply_capabilities(
4706 base_runtime_agent,
4707 &[
4708 "current_time".to_string(),
4709 "openai_tool_search".to_string(),
4710 "test_math".to_string(),
4711 ],
4712 ®istry,
4713 &test_ctx(),
4714 )
4715 .await;
4716
4717 assert!(applied.tool_registry.has("get_current_time"));
4719 assert!(applied.tool_registry.has("add"));
4720 assert!(applied.tool_registry.has("subtract"));
4721 assert!(applied.tool_registry.has("multiply"));
4722 assert!(applied.tool_registry.has("divide"));
4723
4724 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4726 assert!(ts.enabled);
4727 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4728 }
4729
4730 #[tokio::test]
4731 async fn test_collect_capabilities_tool_search_custom_threshold() {
4732 let registry = CapabilityRegistry::with_builtins();
4733
4734 let configs = vec![AgentCapabilityConfig {
4735 capability_ref: CapabilityId::new("openai_tool_search"),
4736 config: serde_json::json!({"threshold": 5}),
4737 }];
4738
4739 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4740
4741 let ts = collected.tool_search.as_ref().unwrap();
4742 assert!(ts.enabled);
4743 assert_eq!(ts.threshold, 5);
4744 }
4745
4746 #[tokio::test]
4747 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
4748 let registry = CapabilityRegistry::with_builtins();
4749
4750 let configs = vec![
4751 AgentCapabilityConfig {
4752 capability_ref: CapabilityId::new("auto_tool_search"),
4753 config: serde_json::json!({"threshold": 2}),
4754 },
4755 AgentCapabilityConfig {
4756 capability_ref: CapabilityId::new("test_math"),
4757 config: serde_json::json!({}),
4758 },
4759 ];
4760
4761 let ctx = test_ctx().with_model("claude-3-5-haiku");
4765 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4766
4767 assert!(
4768 collected.tool_search.is_none(),
4769 "auto_tool_search must not set a hosted config on a non-native model"
4770 );
4771 assert!(
4772 collected
4773 .tools
4774 .iter()
4775 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4776 "auto_tool_search must contribute the client-side tool_search tool"
4777 );
4778 assert!(
4779 !collected.tool_definition_hooks.is_empty(),
4780 "auto_tool_search must contribute a client-side deferral hook"
4781 );
4782
4783 let mut transformed = collected.tool_definitions.clone();
4784 for hook in &collected.tool_definition_hooks {
4785 transformed = hook.transform(transformed);
4786 }
4787 let add_tool = transformed
4788 .iter()
4789 .find(|tool| tool.name() == "add")
4790 .expect("test_math contributes add");
4791 assert!(
4792 add_tool.parameters().get("properties").is_none(),
4793 "generic auto_tool_search must honor the configured threshold"
4794 );
4795 }
4796
4797 #[tokio::test]
4798 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
4799 let registry = CapabilityRegistry::with_builtins();
4800
4801 let configs = vec![AgentCapabilityConfig {
4802 capability_ref: CapabilityId::new("auto_tool_search"),
4803 config: serde_json::json!({"threshold": 7}),
4804 }];
4805
4806 let ctx = test_ctx().with_model("gpt-5.4");
4809 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4810
4811 let ts = collected
4812 .tool_search
4813 .as_ref()
4814 .expect("auto_tool_search must set a hosted config on a native model");
4815 assert!(ts.enabled);
4816 assert_eq!(ts.threshold, 7);
4817 assert!(
4818 !collected
4819 .tools
4820 .iter()
4821 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4822 "hosted mechanism must not contribute the client-side tool_search tool"
4823 );
4824 assert!(
4825 collected.tool_definition_hooks.is_empty(),
4826 "hosted mechanism must not contribute a client-side deferral hook"
4827 );
4828 }
4829
4830 #[tokio::test]
4831 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
4832 let registry = CapabilityRegistry::with_builtins();
4833
4834 let configs = vec![AgentCapabilityConfig {
4835 capability_ref: CapabilityId::new("auto_tool_search"),
4836 config: serde_json::json!({"threshold": 9}),
4837 }];
4838
4839 let ctx = test_ctx().with_model("claude-opus-4-8");
4842 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4843
4844 let ts = collected
4845 .tool_search
4846 .as_ref()
4847 .expect("auto_tool_search must set a hosted config on a native Claude model");
4848 assert!(ts.enabled);
4849 assert_eq!(ts.threshold, 9);
4850 assert!(
4851 !collected
4852 .tools
4853 .iter()
4854 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4855 "hosted mechanism must not contribute the client-side tool_search tool"
4856 );
4857 assert!(
4858 collected.tool_definition_hooks.is_empty(),
4859 "hosted mechanism must not contribute a client-side deferral hook"
4860 );
4861 }
4862
4863 #[tokio::test]
4864 async fn test_collect_capabilities_no_tool_search_without_capability() {
4865 let registry = CapabilityRegistry::with_builtins();
4866
4867 let configs = vec![AgentCapabilityConfig {
4868 capability_ref: CapabilityId::new("current_time"),
4869 config: serde_json::json!({}),
4870 }];
4871
4872 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4873
4874 assert!(collected.tool_search.is_none());
4875 }
4876
4877 #[tokio::test]
4878 async fn test_collect_capabilities_tool_search_category_propagation() {
4879 let registry = CapabilityRegistry::with_builtins();
4880
4881 let configs = vec![
4883 AgentCapabilityConfig {
4884 capability_ref: CapabilityId::new("test_math"),
4885 config: serde_json::json!({}),
4886 },
4887 AgentCapabilityConfig {
4888 capability_ref: CapabilityId::new("openai_tool_search"),
4889 config: serde_json::json!({}),
4890 },
4891 ];
4892
4893 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4894
4895 assert!(collected.tool_search.is_some());
4897
4898 for tool_def in &collected.tool_definitions {
4900 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
4902 assert!(
4903 tool_def.category().is_some(),
4904 "Tool {} should have a category from its capability",
4905 tool_def.name()
4906 );
4907 }
4908 }
4909 }
4910
4911 #[tokio::test]
4912 async fn test_apply_capabilities_prompt_caching() {
4913 let registry = CapabilityRegistry::with_builtins();
4914 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4915
4916 let applied = apply_capabilities(
4917 base_runtime_agent.clone(),
4918 &["prompt_caching".to_string()],
4919 ®istry,
4920 &test_ctx(),
4921 )
4922 .await;
4923
4924 assert_eq!(
4925 applied.runtime_agent.system_prompt,
4926 base_runtime_agent.system_prompt
4927 );
4928 assert!(applied.tool_registry.is_empty());
4929 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
4930
4931 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
4932 assert!(prompt_cache.enabled);
4933 assert_eq!(
4934 prompt_cache.strategy,
4935 crate::driver_registry::PromptCacheStrategy::Auto
4936 );
4937 assert!(prompt_cache.gemini_cached_content.is_none());
4938 }
4939
4940 #[tokio::test]
4941 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
4942 let registry = CapabilityRegistry::with_builtins();
4943
4944 let configs = vec![AgentCapabilityConfig {
4945 capability_ref: CapabilityId::new("prompt_caching"),
4946 config: serde_json::json!({"strategy": "auto"}),
4947 }];
4948
4949 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4950
4951 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
4952 assert!(prompt_cache.enabled);
4953 assert_eq!(
4954 prompt_cache.strategy,
4955 crate::driver_registry::PromptCacheStrategy::Auto
4956 );
4957 assert!(prompt_cache.gemini_cached_content.is_none());
4958 }
4959
4960 #[tokio::test]
4961 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
4962 let registry = CapabilityRegistry::with_builtins();
4963
4964 let configs = vec![AgentCapabilityConfig {
4965 capability_ref: CapabilityId::new("prompt_caching"),
4966 config: serde_json::json!({
4967 "strategy": "auto",
4968 "gemini_cached_content": "cachedContents/demo-cache"
4969 }),
4970 }];
4971
4972 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4973
4974 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
4975 assert_eq!(
4976 prompt_cache.gemini_cached_content.as_deref(),
4977 Some("cachedContents/demo-cache")
4978 );
4979 }
4980
4981 struct SkillContributingCapability;
4986
4987 impl Capability for SkillContributingCapability {
4988 fn id(&self) -> &str {
4989 "contributes_skills"
4990 }
4991 fn name(&self) -> &str {
4992 "Contributes Skills"
4993 }
4994 fn description(&self) -> &str {
4995 "Test capability that contributes skills."
4996 }
4997 fn contribute_skills(&self) -> Vec<SkillContribution> {
4998 vec![
4999 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
5000 .with_files(vec![(
5001 "scripts/a.sh".to_string(),
5002 "#!/bin/sh\necho a\n".to_string(),
5003 )]),
5004 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
5005 .with_user_invocable(false),
5006 ]
5007 }
5008 }
5009
5010 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
5011 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
5012 MountSource::InlineFile { content, .. } => content.as_str(),
5013 _ => panic!("Expected InlineFile for SKILL.md"),
5014 }
5015 }
5016
5017 #[tokio::test]
5018 async fn test_contribute_skills_normalized_to_mounts() {
5019 let mut registry = CapabilityRegistry::new();
5020 registry.register(SkillContributingCapability);
5021
5022 let configs = vec![AgentCapabilityConfig {
5023 capability_ref: CapabilityId::new("contributes_skills"),
5024 config: serde_json::json!({}),
5025 }];
5026
5027 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5028
5029 let skill_mounts: Vec<_> = collected
5030 .mounts
5031 .iter()
5032 .filter(|m| m.path.starts_with("/.agents/skills/"))
5033 .collect();
5034 assert_eq!(skill_mounts.len(), 2);
5035
5036 for m in &skill_mounts {
5039 assert!(m.is_readonly());
5040 assert_eq!(m.capability_id, "contributes_skills");
5041 }
5042
5043 let alpha = skill_mounts
5044 .iter()
5045 .find(|m| m.path == "/.agents/skills/alpha-skill")
5046 .expect("alpha-skill mount missing");
5047 match &alpha.source {
5048 MountSource::InlineDirectory { entries } => {
5049 assert!(entries.contains_key("SKILL.md"));
5050 assert!(entries.contains_key("scripts/a.sh"));
5051 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5052 assert_eq!(parsed.name, "alpha-skill");
5053 assert!(parsed.user_invocable);
5054 }
5055 _ => panic!("Expected InlineDirectory"),
5056 }
5057
5058 let beta = skill_mounts
5059 .iter()
5060 .find(|m| m.path == "/.agents/skills/beta-skill")
5061 .expect("beta-skill mount missing");
5062 match &beta.source {
5063 MountSource::InlineDirectory { entries } => {
5064 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5065 assert!(!parsed.user_invocable);
5066 }
5067 _ => panic!("Expected InlineDirectory"),
5068 }
5069 }
5070
5071 #[tokio::test]
5072 async fn test_contribute_skills_default_empty() {
5073 let mut registry = CapabilityRegistry::new();
5076 registry.register(FilterTestCapability { priority: 0 });
5077
5078 let configs = vec![AgentCapabilityConfig {
5079 capability_ref: CapabilityId::new("filter_test"),
5080 config: serde_json::json!({}),
5081 }];
5082
5083 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5084 assert!(
5085 collected
5086 .mounts
5087 .iter()
5088 .all(|m| !m.path.starts_with("/.agents/skills/"))
5089 );
5090 }
5091
5092 struct LocalizedCapability;
5093
5094 impl Capability for LocalizedCapability {
5095 fn id(&self) -> &str {
5096 "localized"
5097 }
5098 fn name(&self) -> &str {
5099 "Localized"
5100 }
5101 fn description(&self) -> &str {
5102 "English description"
5103 }
5104 fn localizations(&self) -> Vec<CapabilityLocalization> {
5105 vec![
5106 CapabilityLocalization {
5107 locale: "en",
5108 name: None,
5109 description: None,
5110 config_description: Some("Controls things."),
5111 config_overlay: None,
5112 },
5113 CapabilityLocalization {
5114 locale: "uk",
5115 name: Some("Локалізована"),
5116 description: Some("Український опис"),
5117 config_description: Some("Керує налаштуваннями."),
5118 config_overlay: None,
5119 },
5120 ]
5121 }
5122 }
5123
5124 #[test]
5125 fn localized_name_falls_back_exact_language_then_base() {
5126 let cap = LocalizedCapability;
5127 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
5129 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
5130 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
5132 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
5134 assert_eq!(cap.localized_name(None), "Localized");
5135 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
5136 assert_eq!(cap.localized_description(Some("de")), "English description");
5137 }
5138
5139 #[test]
5140 fn describe_schema_resolves_config_description_per_locale() {
5141 let cap = LocalizedCapability;
5142 assert_eq!(
5143 cap.describe_schema(Some("uk-UA")).as_deref(),
5144 Some("Керує налаштуваннями.")
5145 );
5146 assert_eq!(
5148 cap.describe_schema(Some("pl")).as_deref(),
5149 Some("Controls things.")
5150 );
5151 assert_eq!(
5152 cap.describe_schema(None).as_deref(),
5153 Some("Controls things.")
5154 );
5155 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
5157 }
5158}