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 knowledge_index;
112mod loop_detection;
113mod lua;
114mod lua_code_mode;
115pub mod mcp;
116mod memory;
117mod message_metadata;
118mod model_scout;
119mod monitors;
120mod noop;
121mod openai_tool_search;
122mod openrouter_server_tools;
123mod openrouter_workspace;
124#[cfg(feature = "ui-capabilities")]
125mod openui;
126mod platform_management;
127mod prompt_caching;
128mod prompt_canary_guardrail;
129mod research;
130mod sample_data;
131mod self_budget;
132mod session;
133mod session_sandbox;
134mod session_schedule;
135mod session_sql_database;
136mod session_storage;
137mod session_tasks;
138mod skills;
139mod skills_scoped;
140mod stateless_todo_list;
141mod subagents;
142mod system_commands;
143mod test_math;
144mod test_weather;
145mod tool_call_repair;
146mod tool_output_distillation;
147mod tool_output_persistence;
148mod tool_search;
149pub mod user_hooks;
150mod web_fetch;
151
152pub use a2a_delegation::{
154 A2A_AGENT_DELEGATION_CAPABILITY_ID, A2aAgentDelegationCapability, SpawnAgentTool,
155};
156#[cfg(feature = "ui-capabilities")]
157pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
158pub use agent_handoff::{
159 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
160 MessageAgentHandoffTool, StartAgentHandoffTool,
161};
162pub use agent_instructions::{
163 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
164 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
165 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
166};
167pub use attach_skill::{
168 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
169 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
170 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
171};
172pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
173pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
174pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
175pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
176pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
177pub use compaction::{
178 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
179 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
180 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
181 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
182 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
183 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
184 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
185};
186pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
187pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
188pub use declarative::{
189 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
190 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
191 declarative_capability_info, hydrate_declarative_capability_config,
192 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
193 plugin_capability_info, validate_declarative_capability_definition,
194};
195pub use error_disclosure::{
196 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
197};
198pub use fake_aws::{
199 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
200 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
201 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
202 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
203};
204pub use fake_crm::{
205 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
206 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
207 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
208};
209pub use fake_financial::{
210 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
211 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
212 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
213 FinanceListTransactionsTool,
214};
215pub use fake_warehouse::{
216 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
217 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
218 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
219 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
220};
221pub use file_system::{
222 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
223 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
224};
225pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
226pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
227pub use infinity_context::{
228 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
229};
230pub use knowledge_base::{
231 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
232 validate_knowledge_base_config,
233};
234pub use knowledge_index::{
235 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
236 validate_knowledge_index_config,
237};
238pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
239pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
240pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
241pub use mcp::{
242 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
243 parse_mcp_capability_id,
244};
245pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
246pub use message_metadata::{
247 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
248 MessageMetadataField, render_annotation,
249};
250pub use model_scout::{
251 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
252 RouterUpdateProposal, compute_score, rank_results,
253};
254pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
255pub use openai_tool_search::{
256 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
257 model_supports_native_tool_search,
258};
259pub use openrouter_server_tools::{
260 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
261};
262pub use openrouter_workspace::{
263 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
264 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
265 detect_policy_drift,
266};
267#[cfg(feature = "ui-capabilities")]
268pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
269pub use platform_management::{
270 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
271 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
272 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
273};
274pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
275pub use prompt_canary_guardrail::{
276 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
277 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
278 REASON_CODE_SYSTEM_PROMPT_LEAK,
279};
280pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
281pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
282pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
283pub use session::{
284 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
285};
286pub use session_sandbox::{
287 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
288 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
289};
290pub use session_schedule::{
291 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
292 SessionScheduleCapability,
293};
294pub use session_sql_database::{
295 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
296 SqlSchemaTool,
297};
298pub use session_storage::{
299 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
300 is_internal_session_kv_key,
301};
302pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
303pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
304pub use skills_scoped::{
305 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
306};
307pub use stateless_todo_list::{
308 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
309};
310pub use subagents::{SUBAGENTS_CAPABILITY_ID, SubagentCapability};
311pub use bashkit_shell::{
313 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
314};
315pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
316pub use test_math::{
317 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
318};
319pub use test_weather::{
320 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
321};
322pub use tool_call_repair::{
323 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
324 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
325 salvage_tool_arguments, tool_call_repair_capability,
326};
327pub use tool_output_distillation::{
328 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
329};
330pub use tool_output_persistence::{
331 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
332};
333pub use tool_search::{
334 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
335};
336pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
337pub use web_fetch::{
338 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
339 derive_bot_auth_public_key,
340};
341
342pub struct SystemPromptContext {
352 pub session_id: SessionId,
354 pub locale: Option<String>,
356 pub file_store: Option<Arc<dyn SessionFileSystem>>,
358 pub model: Option<String>,
364}
365
366impl SystemPromptContext {
367 pub fn without_file_store(session_id: SessionId) -> Self {
369 Self {
370 session_id,
371 locale: None,
372 file_store: None,
373 model: None,
374 }
375 }
376
377 pub fn with_model(mut self, model: impl Into<String>) -> Self {
379 self.model = Some(model.into());
380 self
381 }
382}
383
384#[derive(Debug, Clone)]
436pub struct CapabilityLocalization {
437 pub locale: &'static str,
439 pub name: Option<&'static str>,
441 pub description: Option<&'static str>,
443 pub config_description: Option<&'static str>,
448 pub config_overlay: Option<serde_json::Value>,
454}
455
456impl CapabilityLocalization {
457 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
459 Self {
460 locale,
461 name: Some(name),
462 description: Some(description),
463 config_description: None,
464 config_overlay: None,
465 }
466 }
467}
468
469pub fn resolve_localized_field<T>(
473 localizations: &[CapabilityLocalization],
474 locale: Option<&str>,
475 field: impl Fn(&CapabilityLocalization) -> Option<T>,
476) -> Option<T> {
477 let mut candidates: Vec<String> = Vec::new();
478 if let Some(raw) = locale {
479 let normalized = raw.trim().replace('_', "-").to_lowercase();
480 if !normalized.is_empty() {
481 if let Some((language, _)) = normalized.split_once('-') {
482 let language = language.to_string();
483 candidates.push(normalized);
484 candidates.push(language);
485 } else {
486 candidates.push(normalized);
487 }
488 }
489 }
490 candidates.push("en".to_string());
491
492 for candidate in candidates {
493 let hit = localizations
494 .iter()
495 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
496 .and_then(&field);
497 if hit.is_some() {
498 return hit;
499 }
500 }
501 None
502}
503
504#[async_trait]
505pub trait Capability: Send + Sync {
506 fn id(&self) -> &str;
508
509 fn aliases(&self) -> Vec<&'static str> {
518 vec![]
519 }
520
521 fn name(&self) -> &str;
523
524 fn description(&self) -> &str;
526
527 fn localizations(&self) -> Vec<CapabilityLocalization> {
532 vec![]
533 }
534
535 fn localized_name(&self, locale: Option<&str>) -> String {
538 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
539 .unwrap_or_else(|| self.name())
540 .to_string()
541 }
542
543 fn localized_description(&self, locale: Option<&str>) -> String {
545 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
546 .unwrap_or_else(|| self.description())
547 .to_string()
548 }
549
550 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
554 resolve_localized_field(&self.localizations(), locale, |entry| {
555 entry.config_description
556 })
557 .map(str::to_string)
558 }
559
560 fn status(&self) -> CapabilityStatus {
562 CapabilityStatus::Available
563 }
564
565 fn icon(&self) -> Option<&str> {
567 None
568 }
569
570 fn category(&self) -> Option<&str> {
572 None
573 }
574
575 fn is_guardrail(&self) -> bool {
580 false
581 }
582
583 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
594 None
595 }
596
597 fn system_prompt_addition(&self) -> Option<&str> {
617 None
618 }
619
620 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
632 self.system_prompt_addition().map(|addition| {
633 format!(
634 "<capability id=\"{}\">\n{}\n</capability>",
635 self.id(),
636 addition
637 )
638 })
639 }
640
641 fn system_prompt_preview(&self) -> Option<String> {
647 self.system_prompt_addition().map(|s| s.to_string())
648 }
649
650 fn tools(&self) -> Vec<Box<dyn Tool>> {
652 vec![]
653 }
654
655 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
663 self.tools()
664 }
665
666 async fn system_prompt_contribution_with_config(
673 &self,
674 ctx: &SystemPromptContext,
675 _config: &serde_json::Value,
676 ) -> Option<String> {
677 self.system_prompt_contribution(ctx).await
678 }
679
680 fn tool_definitions(&self) -> Vec<ToolDefinition> {
683 self.tools().iter().map(|t| t.to_definition()).collect()
684 }
685
686 fn mounts(&self) -> Vec<MountPoint> {
694 vec![]
695 }
696
697 fn dependencies(&self) -> Vec<&'static str> {
706 vec![]
707 }
708
709 fn features(&self) -> Vec<&'static str> {
724 vec![]
725 }
726
727 fn config_schema(&self) -> Option<serde_json::Value> {
733 None
734 }
735
736 fn config_ui_schema(&self) -> Option<serde_json::Value> {
741 None
742 }
743
744 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
750 Ok(())
751 }
752
753 fn mcp_servers(&self) -> ScopedMcpServers {
759 ScopedMcpServers::default()
760 }
761
762 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
764 self.mcp_servers()
765 }
766
767 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
780 None
781 }
782
783 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
791 None
792 }
793
794 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
805 vec![]
806 }
807
808 fn pre_tool_use_hooks_with_config(
813 &self,
814 _config: &serde_json::Value,
815 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
816 self.pre_tool_use_hooks()
817 }
818
819 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
827 vec![]
828 }
829
830 fn post_tool_exec_hooks_with_config(
835 &self,
836 _config: &serde_json::Value,
837 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
838 self.post_tool_exec_hooks()
839 }
840
841 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
850 vec![]
851 }
852
853 fn tool_definition_hooks_with_config(
858 &self,
859 _config: &serde_json::Value,
860 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
861 self.tool_definition_hooks()
862 }
863
864 fn tool_definition_hooks_with_context(
874 &self,
875 _ctx: &SystemPromptContext,
876 config: &serde_json::Value,
877 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
878 self.tool_definition_hooks_with_config(config)
879 }
880
881 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
889 vec![]
890 }
891
892 fn narrate(
906 &self,
907 _tool_def: Option<&ToolDefinition>,
908 tool_call: &ToolCall,
909 phase: crate::tool_narration::ToolNarrationPhase,
910 locale: Option<&str>,
911 ) -> Option<String> {
912 self.tools()
913 .iter()
914 .find(|tool| tool.name() == tool_call.name)
915 .and_then(|tool| tool.narrate(tool_call, phase, locale))
916 }
917
918 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
934 vec![]
935 }
936
937 fn user_hooks_with_config(
943 &self,
944 _config: &serde_json::Value,
945 ) -> Vec<crate::user_hook_types::UserHookSpec> {
946 self.user_hooks()
947 }
948
949 fn risk_level(&self) -> RiskLevel {
957 RiskLevel::Low
958 }
959
960 fn commands(&self) -> Vec<CommandDescriptor> {
968 vec![]
969 }
970
971 async fn execute_command(
985 &self,
986 request: &ExecuteCommandRequest,
987 _ctx: &CommandExecutionContext,
988 ) -> crate::error::Result<CommandResult> {
989 Err(crate::error::AgentLoopError::config(format!(
990 "capability {} declared command /{} but does not implement execute_command",
991 self.id(),
992 request.name,
993 )))
994 }
995
996 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1004 vec![]
1005 }
1006
1007 fn contribute_skills(&self) -> Vec<SkillContribution> {
1017 vec![]
1018 }
1019
1020 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1031 vec![]
1032 }
1033}
1034
1035pub trait ToolDefinitionHook: Send + Sync {
1036 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1037
1038 fn applies_with_native_tool_search(&self) -> bool {
1043 true
1044 }
1045}
1046
1047pub trait ToolCallHook: Send + Sync {
1048 fn narration(
1049 &self,
1050 _tool_def: Option<&ToolDefinition>,
1051 _tool_call: &ToolCall,
1052 _phase: crate::tool_narration::ToolNarrationPhase,
1053 _locale: Option<&str>,
1054 ) -> Option<String> {
1055 None
1056 }
1057
1058 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1059 tool_call
1060 }
1061}
1062
1063pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1069
1070impl ToolCallHook for CapabilityNarrationHook {
1071 fn narration(
1072 &self,
1073 tool_def: Option<&ToolDefinition>,
1074 tool_call: &ToolCall,
1075 phase: crate::tool_narration::ToolNarrationPhase,
1076 locale: Option<&str>,
1077 ) -> Option<String> {
1078 self.0.narrate(tool_def, tool_call, phase, locale)
1079 }
1080}
1081
1082#[derive(
1086 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1087)]
1088#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1089#[cfg_attr(feature = "openapi", schema(example = "low"))]
1090#[serde(rename_all = "lowercase")]
1091pub enum RiskLevel {
1092 Low,
1094 Medium,
1096 High,
1098}
1099
1100#[derive(Debug, Clone, Serialize, Deserialize)]
1106#[serde(rename_all = "snake_case")]
1107pub enum BlueprintModel {
1108 Fixed(String),
1110 Default(String),
1112 Inherit,
1114}
1115
1116pub struct AgentBlueprint {
1122 pub id: &'static str,
1124 pub name: &'static str,
1126 pub description: &'static str,
1128 pub model: BlueprintModel,
1130 pub system_prompt: &'static str,
1132 pub tools: Vec<Box<dyn Tool>>,
1134 pub max_turns: Option<usize>,
1136 pub config_schema: Option<serde_json::Value>,
1138}
1139
1140impl AgentBlueprint {
1141 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1143 self.tools.iter().map(|t| t.to_definition()).collect()
1144 }
1145}
1146
1147impl std::fmt::Debug for AgentBlueprint {
1148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1149 f.debug_struct("AgentBlueprint")
1150 .field("id", &self.id)
1151 .field("name", &self.name)
1152 .field("model", &self.model)
1153 .field("tool_count", &self.tools.len())
1154 .field("max_turns", &self.max_turns)
1155 .finish()
1156 }
1157}
1158
1159#[derive(Clone)]
1186pub struct CapabilityRegistry {
1187 capabilities: HashMap<String, Arc<dyn Capability>>,
1188 aliases: HashMap<String, String>,
1190}
1191
1192impl CapabilityRegistry {
1193 pub fn new() -> Self {
1195 Self {
1196 capabilities: HashMap::new(),
1197 aliases: HashMap::new(),
1198 }
1199 }
1200
1201 pub fn with_builtins() -> Self {
1206 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1207 }
1208
1209 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1214 let mut registry = Self::new();
1215
1216 registry.register(AgentInstructionsCapability);
1218 registry.register(HumanIntentCapability);
1219 registry.register(NoopCapability);
1220 registry.register(CurrentTimeCapability);
1221 registry.register(MessageMetadataCapability);
1222 registry.register(ResearchCapability);
1223 registry.register(ModelScoutCapability);
1224 registry.register(OpenRouterWorkspaceCapability);
1225 registry.register(OpenRouterServerToolsCapability);
1226 registry.register(PlatformManagementCapability);
1227 registry.register(FileSystemCapability);
1228 registry.register(MemoryCapability);
1229 registry.register(SessionStorageCapability);
1230 registry.register(SessionCapability);
1231 registry.register(SessionSqlDatabaseCapability);
1232 registry.register(TestMathCapability);
1233 registry.register(TestWeatherCapability);
1234 registry.register(StatelessTodoListCapability);
1235 registry.register(WebFetchCapability::from_env());
1236 registry.register(BashkitShellCapability);
1237 registry.register(BackgroundExecutionCapability);
1238 registry.register(SessionScheduleCapability);
1239 registry.register(BtwCapability);
1240 registry.register(InfinityContextCapability);
1241 registry.register(budgeting::BudgetingCapability);
1242 registry.register(SelfBudgetCapability);
1243 registry.register(CompactionCapability);
1244 registry.register(ErrorDisclosureCapability);
1245
1246 registry.register(OpenAiToolSearchCapability::new());
1248 registry.register(ClaudeToolSearchCapability::new());
1250 registry.register(ToolSearchCapability::new());
1252 registry.register(AutoToolSearchCapability::new());
1254 registry.register(PromptCachingCapability::new());
1255
1256 registry.register(SkillsCapability);
1258
1259 registry.register(SubagentCapability);
1261
1262 registry.register(SessionTasksCapability);
1264
1265 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1269 registry.register(AgentHandoffCapability);
1270 registry.register(A2aAgentDelegationCapability);
1271 }
1272
1273 registry.register(SystemCommandsCapability);
1275
1276 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1278 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1279
1280 registry.register(user_hooks::UserHooksCapability);
1283
1284 registry.register(LoopDetectionCapability);
1286
1287 registry.register(ToolCallRepairCapability);
1291
1292 registry.register(PromptCanaryGuardrailCapability);
1295
1296 registry.register(GuardrailsCapability);
1299
1300 #[cfg(feature = "ui-capabilities")]
1302 {
1303 registry.register(OpenUiCapability);
1304 registry.register(A2UiCapability);
1305 }
1306
1307 registry.register(SampleDataCapability);
1309
1310 registry.register(DataKnowledgeCapability);
1312
1313 registry.register(KnowledgeBaseCapability);
1315
1316 registry.register(KnowledgeIndexCapability);
1318
1319 registry.register(FakeWarehouseCapability);
1321 registry.register(FakeAwsCapability);
1322 registry.register(FakeCrmCapability);
1323 registry.register(FakeFinancialCapability);
1324
1325 let internal_flags = crate::InternalFeatureFlags::from_env();
1327 if internal_flags.session_sandbox {
1328 registry.register(SessionSandboxCapability);
1329 }
1330
1331 if internal_flags.lua {
1335 registry.register(LuaCapability);
1336 registry.register(LuaCodeModeCapability);
1339 }
1340 for plugin in inventory::iter::<IntegrationPlugin>() {
1341 if (!plugin.experimental_only || grade.experimental_features_enabled())
1342 && plugin
1343 .feature_flag
1344 .is_none_or(|f| internal_flags.is_enabled(f))
1345 {
1346 registry.register_boxed((plugin.factory)());
1347 }
1348 }
1349
1350 registry
1351 }
1352
1353 pub fn register(&mut self, capability: impl Capability + 'static) {
1355 self.register_arc(Arc::new(capability));
1356 }
1357
1358 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1360 self.register_arc(Arc::from(capability));
1361 }
1362
1363 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1365 let canonical = capability.id().to_string();
1366 for alias in capability.aliases() {
1367 self.aliases.insert(alias.to_string(), canonical.clone());
1368 }
1369 self.capabilities.insert(canonical, capability);
1370 }
1371
1372 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1374 self.capabilities
1375 .get(id)
1376 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1377 }
1378
1379 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1384 if self.capabilities.contains_key(id) {
1385 Some(id)
1386 } else {
1387 self.aliases
1388 .get(id)
1389 .filter(|c| self.capabilities.contains_key(*c))
1390 .map(String::as_str)
1391 }
1392 }
1393
1394 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1396 let canonical = self.canonical_id(id)?.to_string();
1397 let removed = self.capabilities.remove(&canonical);
1398 self.aliases.retain(|_, target| *target != canonical);
1399 removed
1400 }
1401
1402 pub fn has(&self, id: &str) -> bool {
1404 self.get(id).is_some()
1405 }
1406
1407 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1409 self.capabilities.values().collect()
1410 }
1411
1412 pub fn len(&self) -> usize {
1414 self.capabilities.len()
1415 }
1416
1417 pub fn is_empty(&self) -> bool {
1419 self.capabilities.is_empty()
1420 }
1421
1422 pub fn builder() -> CapabilityRegistryBuilder {
1424 CapabilityRegistryBuilder::new()
1425 }
1426
1427 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1431 for cap in self.capabilities.values() {
1432 for bp in cap.agent_blueprints() {
1433 if bp.id == id {
1434 return Some(bp);
1435 }
1436 }
1437 }
1438 None
1439 }
1440
1441 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1445 for (capability_id, cap) in &self.capabilities {
1446 for bp in cap.agent_blueprints() {
1447 if bp.id == id {
1448 return Some((capability_id.clone(), bp));
1449 }
1450 }
1451 }
1452 None
1453 }
1454
1455 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1457 self.capabilities
1458 .values()
1459 .flat_map(|cap| cap.agent_blueprints())
1460 .collect()
1461 }
1462}
1463
1464impl Default for CapabilityRegistry {
1465 fn default() -> Self {
1466 Self::with_builtins()
1467 }
1468}
1469
1470impl std::fmt::Debug for CapabilityRegistry {
1471 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1472 let ids: Vec<_> = self.capabilities.keys().collect();
1473 f.debug_struct("CapabilityRegistry")
1474 .field("capabilities", &ids)
1475 .finish()
1476 }
1477}
1478
1479pub struct CapabilityRegistryBuilder {
1481 registry: CapabilityRegistry,
1482}
1483
1484impl CapabilityRegistryBuilder {
1485 pub fn new() -> Self {
1487 Self {
1488 registry: CapabilityRegistry::new(),
1489 }
1490 }
1491
1492 pub fn with_builtins() -> Self {
1494 Self {
1495 registry: CapabilityRegistry::with_builtins(),
1496 }
1497 }
1498
1499 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1501 self.registry.register(capability);
1502 self
1503 }
1504
1505 pub fn build(self) -> CapabilityRegistry {
1507 self.registry
1508 }
1509}
1510
1511impl Default for CapabilityRegistryBuilder {
1512 fn default() -> Self {
1513 Self::new()
1514 }
1515}
1516
1517pub struct ModelViewContext<'a> {
1523 pub session_id: SessionId,
1524 pub prior_usage: Option<&'a TokenUsage>,
1525}
1526
1527pub trait ModelViewProvider: Send + Sync {
1533 fn apply_model_view(
1534 &self,
1535 messages: Vec<Message>,
1536 config: &serde_json::Value,
1537 context: &ModelViewContext<'_>,
1538 ) -> Vec<Message>;
1539
1540 fn priority(&self) -> i32 {
1541 0
1542 }
1543}
1544
1545pub struct CollectedCapabilities {
1550 pub system_prompt_parts: Vec<String>,
1552 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1554 pub tools: Vec<Box<dyn Tool>>,
1556 pub tool_definitions: Vec<ToolDefinition>,
1558 pub mounts: Vec<MountPoint>,
1560 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1562 pub applied_ids: Vec<String>,
1564 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1566 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1568 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1571 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1573 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1575 pub mcp_servers: ScopedMcpServers,
1577 }
1583
1584#[derive(Debug, Clone, PartialEq, Eq)]
1585pub struct SystemPromptAttribution {
1586 pub capability_id: String,
1587 pub content: String,
1588}
1589
1590impl CollectedCapabilities {
1591 pub fn system_prompt_prefix(&self) -> Option<String> {
1594 if self.system_prompt_parts.is_empty() {
1595 None
1596 } else {
1597 Some(self.system_prompt_parts.join("\n\n"))
1598 }
1599 }
1600
1601 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1605 for (provider, config) in &self.message_filter_providers {
1607 provider.apply_filters(query, config);
1608 }
1609 }
1610
1611 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1614 for (provider, config) in &self.message_filter_providers {
1615 provider.post_load(messages, config);
1616 }
1617 }
1618
1619 pub fn has_message_filters(&self) -> bool {
1621 !self.message_filter_providers.is_empty()
1622 }
1623}
1624
1625pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1630 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1631 return base_system_prompt.to_string();
1632 };
1633
1634 if base_system_prompt.is_empty() {
1635 return additions.to_string();
1636 }
1637
1638 if base_system_prompt.contains("<system-prompt>") {
1639 format!("{base_system_prompt}\n\n{additions}")
1640 } else {
1641 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1642 }
1643}
1644
1645pub struct CollectedMessageFilters {
1652 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1654}
1655
1656pub struct CollectedModelViewProviders {
1658 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1660}
1661
1662impl CollectedMessageFilters {
1668 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1670 for (provider, config) in &self.message_filter_providers {
1671 provider.apply_filters(query, config);
1672 }
1673 }
1674
1675 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1677 for (provider, config) in &self.message_filter_providers {
1678 provider.post_load(messages, config);
1679 }
1680 }
1681}
1682
1683impl CollectedModelViewProviders {
1684 pub fn apply_model_view(
1686 &self,
1687 mut messages: Vec<Message>,
1688 context: &ModelViewContext<'_>,
1689 ) -> Vec<Message> {
1690 for (provider, config) in &self.model_view_providers {
1691 messages = provider.apply_model_view(messages, config, context);
1692 }
1693 messages
1694 }
1695}
1696
1697fn compaction_is_enabled(
1703 capability_configs: &[AgentCapabilityConfig],
1704 registry: &CapabilityRegistry,
1705) -> bool {
1706 capability_configs.iter().any(|cap_config| {
1707 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
1708 && registry
1709 .get(cap_config.capability_ref.as_str())
1710 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
1711 })
1712}
1713
1714fn message_filter_config_for(
1723 cap_id: &str,
1724 base: &serde_json::Value,
1725 compaction_on: bool,
1726) -> serde_json::Value {
1727 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
1728 return base.clone();
1729 }
1730 let mut config = base.clone();
1731 match config.as_object_mut() {
1732 Some(map) => {
1733 map.insert(
1734 "compaction_active".to_string(),
1735 serde_json::Value::Bool(true),
1736 );
1737 }
1738 None => {
1739 config = serde_json::json!({ "compaction_active": true });
1740 }
1741 }
1742 config
1743}
1744
1745pub fn collect_message_filters_only(
1751 capability_configs: &[AgentCapabilityConfig],
1752 registry: &CapabilityRegistry,
1753) -> CollectedMessageFilters {
1754 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1755 Vec::new();
1756 let compaction_on = compaction_is_enabled(capability_configs, registry);
1757
1758 for cap_config in capability_configs {
1759 let cap_id = cap_config.capability_ref.as_str();
1760 if let Some(capability) = registry.get(cap_id) {
1761 if capability.status() != CapabilityStatus::Available {
1762 continue;
1763 }
1764 let effective: &dyn Capability = capability
1767 .resolve_for_model(None)
1768 .unwrap_or_else(|| capability.as_ref());
1769 if let Some(provider) = effective.message_filter_provider() {
1770 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
1771 message_filter_providers.push((provider, config));
1772 }
1773 }
1774 }
1775
1776 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1777
1778 CollectedMessageFilters {
1779 message_filter_providers,
1780 }
1781}
1782
1783pub fn collect_model_view_providers(
1790 capability_configs: &[AgentCapabilityConfig],
1791 registry: &CapabilityRegistry,
1792 model: Option<&str>,
1793) -> CollectedModelViewProviders {
1794 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1795
1796 for cap_config in capability_configs {
1797 let cap_id = cap_config.capability_ref.as_str();
1798 if let Some(capability) = registry.get(cap_id) {
1799 if capability.status() != CapabilityStatus::Available {
1800 continue;
1801 }
1802 let effective: &dyn Capability = capability
1803 .resolve_for_model(model)
1804 .unwrap_or_else(|| capability.as_ref());
1805 if let Some(provider) = effective.model_view_provider() {
1806 model_view_providers.push((provider, cap_config.config.clone()));
1807 }
1808 }
1809 }
1810
1811 model_view_providers.sort_by_key(|(p, _)| p.priority());
1812
1813 CollectedModelViewProviders {
1814 model_view_providers,
1815 }
1816}
1817
1818pub fn collect_capability_mcp_servers(
1819 capability_configs: &[AgentCapabilityConfig],
1820 registry: &CapabilityRegistry,
1821) -> ScopedMcpServers {
1822 let mut servers = ScopedMcpServers::default();
1823
1824 for cap_config in capability_configs {
1825 let cap_id = cap_config.capability_ref.as_str();
1826 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
1829 if let Ok(definition) =
1830 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1831 {
1832 if definition.status != CapabilityStatus::Available {
1833 continue;
1834 }
1835 if let Some(contributed) = definition.mcp_servers {
1836 servers = merge_scoped_mcp_servers(&servers, &contributed);
1837 }
1838 }
1839 continue;
1840 }
1841 if let Some(capability) = registry.get(cap_id) {
1842 if capability.status() != CapabilityStatus::Available {
1843 continue;
1844 }
1845 servers = merge_scoped_mcp_servers(
1846 &servers,
1847 &capability.mcp_servers_with_config(&cap_config.config),
1848 );
1849 }
1850 }
1851
1852 servers
1853}
1854
1855pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1862
1863#[derive(Debug, Clone, PartialEq, Eq)]
1865pub enum DependencyError {
1866 CircularDependency {
1868 capability_id: String,
1870 chain: Vec<String>,
1872 },
1873 TooManyCapabilities {
1875 count: usize,
1877 max: usize,
1879 },
1880}
1881
1882impl std::fmt::Display for DependencyError {
1883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1884 match self {
1885 DependencyError::CircularDependency {
1886 capability_id,
1887 chain,
1888 } => {
1889 write!(
1890 f,
1891 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1892 capability_id,
1893 chain.join(" -> "),
1894 capability_id
1895 )
1896 }
1897 DependencyError::TooManyCapabilities { count, max } => {
1898 write!(
1899 f,
1900 "Too many capabilities after resolution: {} (max: {})",
1901 count, max
1902 )
1903 }
1904 }
1905 }
1906}
1907
1908impl std::error::Error for DependencyError {}
1909
1910#[derive(Debug, Clone)]
1912pub struct ResolvedCapabilities {
1913 pub resolved_ids: Vec<String>,
1916 pub added_as_dependencies: Vec<String>,
1918 pub user_selected: Vec<String>,
1920}
1921
1922pub fn resolve_dependencies(
1942 selected_ids: &[String],
1943 registry: &CapabilityRegistry,
1944) -> Result<ResolvedCapabilities, DependencyError> {
1945 use std::collections::HashSet;
1946
1947 let user_selected: HashSet<String> = selected_ids
1949 .iter()
1950 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
1951 .collect();
1952 let mut resolved: Vec<String> = Vec::new();
1953 let mut resolved_set: HashSet<String> = HashSet::new();
1954 let mut added_as_dependencies: Vec<String> = Vec::new();
1955
1956 for cap_id in selected_ids {
1958 resolve_single_capability(
1959 cap_id,
1960 registry,
1961 &mut resolved,
1962 &mut resolved_set,
1963 &mut added_as_dependencies,
1964 &user_selected,
1965 &mut Vec::new(), )?;
1967 }
1968
1969 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
1971 return Err(DependencyError::TooManyCapabilities {
1972 count: resolved.len(),
1973 max: MAX_RESOLVED_CAPABILITIES,
1974 });
1975 }
1976
1977 Ok(ResolvedCapabilities {
1978 resolved_ids: resolved,
1979 added_as_dependencies,
1980 user_selected: selected_ids.to_vec(),
1981 })
1982}
1983
1984pub fn resolve_capability_configs(
1989 selected_configs: &[AgentCapabilityConfig],
1990 registry: &CapabilityRegistry,
1991) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
1992 let mut selected_ids: Vec<String> = Vec::new();
1993 for config in selected_configs {
1994 if (is_declarative_capability(config.capability_id())
1997 || is_plugin_capability(config.capability_id()))
1998 && let Ok(definition) =
1999 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2000 {
2001 selected_ids.extend(definition.dependencies);
2002 }
2003 selected_ids.push(config.capability_id().to_string());
2004 }
2005 let resolved = resolve_dependencies(&selected_ids, registry)?;
2006
2007 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2010 .iter()
2011 .map(|config| {
2012 let id = config.capability_id();
2013 let id = registry.canonical_id(id).unwrap_or(id);
2014 (id.to_string(), config.config.clone())
2015 })
2016 .collect();
2017
2018 Ok(resolved
2019 .resolved_ids
2020 .into_iter()
2021 .map(|capability_id| {
2022 explicit_configs
2023 .get(&capability_id)
2024 .cloned()
2025 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2026 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2027 })
2028 .collect())
2029}
2030
2031fn resolve_single_capability(
2033 cap_id: &str,
2034 registry: &CapabilityRegistry,
2035 resolved: &mut Vec<String>,
2036 resolved_set: &mut std::collections::HashSet<String>,
2037 added_as_dependencies: &mut Vec<String>,
2038 user_selected: &std::collections::HashSet<String>,
2039 visiting: &mut Vec<String>,
2040) -> Result<(), DependencyError> {
2041 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2045
2046 if resolved_set.contains(cap_id) {
2048 return Ok(());
2049 }
2050
2051 if visiting.contains(&cap_id.to_string()) {
2053 return Err(DependencyError::CircularDependency {
2054 capability_id: cap_id.to_string(),
2055 chain: visiting.clone(),
2056 });
2057 }
2058
2059 let capability = match registry.get(cap_id) {
2061 Some(cap) => cap,
2062 None => {
2063 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2067 && !resolved_set.contains(cap_id)
2068 {
2069 resolved.push(cap_id.to_string());
2070 resolved_set.insert(cap_id.to_string());
2071 if !user_selected.contains(cap_id) {
2072 added_as_dependencies.push(cap_id.to_string());
2073 }
2074 }
2075 return Ok(());
2076 }
2077 };
2078
2079 visiting.push(cap_id.to_string());
2081
2082 for dep_id in capability.dependencies() {
2084 resolve_single_capability(
2085 dep_id,
2086 registry,
2087 resolved,
2088 resolved_set,
2089 added_as_dependencies,
2090 user_selected,
2091 visiting,
2092 )?;
2093 }
2094
2095 visiting.pop();
2097
2098 if !resolved_set.contains(cap_id) {
2100 resolved.push(cap_id.to_string());
2101 resolved_set.insert(cap_id.to_string());
2102
2103 if !user_selected.contains(cap_id) {
2105 added_as_dependencies.push(cap_id.to_string());
2106 }
2107 }
2108
2109 Ok(())
2110}
2111
2112pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2117 use std::collections::HashSet;
2118
2119 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2120 Ok(resolved) => resolved.resolved_ids,
2121 Err(_) => capability_ids.to_vec(),
2122 };
2123
2124 let mut seen = HashSet::new();
2125 let mut features = Vec::new();
2126 for cap_id in &resolved_ids {
2127 if let Some(cap) = registry.get(cap_id) {
2128 for feature in cap.features() {
2129 if seen.insert(feature) {
2130 features.push(feature.to_string());
2131 }
2132 }
2133 }
2134 }
2135 features
2136}
2137
2138pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2141 registry
2142 .get(cap_id)
2143 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2144 .unwrap_or_default()
2145}
2146
2147pub async fn collect_capabilities(
2163 capability_ids: &[String],
2164 registry: &CapabilityRegistry,
2165 ctx: &SystemPromptContext,
2166) -> CollectedCapabilities {
2167 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2170 Ok(resolved) => resolved.resolved_ids,
2171 Err(e) => {
2172 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2173 capability_ids.to_vec()
2174 }
2175 };
2176
2177 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2179 .iter()
2180 .map(|id| AgentCapabilityConfig {
2181 capability_ref: CapabilityId::new(id),
2182 config: serde_json::Value::Object(serde_json::Map::new()),
2183 })
2184 .collect();
2185
2186 collect_capabilities_with_configs(&configs, registry, ctx).await
2187}
2188
2189pub async fn collect_capabilities_with_configs(
2200 capability_configs: &[AgentCapabilityConfig],
2201 registry: &CapabilityRegistry,
2202 ctx: &SystemPromptContext,
2203) -> CollectedCapabilities {
2204 let mut system_prompt_parts: Vec<String> = Vec::new();
2205 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2206 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2207 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2208 let mut mounts: Vec<MountPoint> = Vec::new();
2209 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2210 Vec::new();
2211 let mut applied_ids: Vec<String> = Vec::new();
2212 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2213 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2214 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2215 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2216 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2217 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2220 let mut mcp_servers = ScopedMcpServers::default();
2221 let compaction_on = compaction_is_enabled(capability_configs, registry);
2222
2223 for cap_config in capability_configs {
2224 let cap_id = cap_config.capability_ref.as_str();
2225 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2230 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2231 cap_config.config.clone(),
2232 ) {
2233 Ok(definition) => {
2234 if definition.status != CapabilityStatus::Available {
2235 continue;
2236 }
2237
2238 if let Some(prompt) = definition.system_prompt.as_deref() {
2239 let contribution =
2240 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2241 system_prompt_attributions.push(SystemPromptAttribution {
2242 capability_id: cap_id.to_string(),
2243 content: contribution.clone(),
2244 });
2245 system_prompt_parts.push(contribution);
2246 }
2247
2248 mounts.extend(definition.mounts(cap_id));
2249 if let Some(ref servers) = definition.mcp_servers {
2250 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2251 }
2252 for skill in definition.skill_contributions() {
2253 mounts.push(skill.to_mount(cap_id));
2254 }
2255
2256 applied_ids.push(cap_id.to_string());
2257 }
2258 Err(error) => {
2259 tracing::warn!(
2260 capability_id = %cap_id,
2261 error = %error,
2262 "Skipping invalid declarative/plugin capability config"
2263 );
2264 }
2265 }
2266 continue;
2267 }
2268 if let Some(capability) = registry.get(cap_id) {
2269 if capability.status() != CapabilityStatus::Available {
2271 continue;
2272 }
2273
2274 let effective: &dyn Capability =
2286 match capability.resolve_for_model(ctx.model.as_deref()) {
2287 Some(inner) => inner,
2288 None => capability.as_ref(),
2289 };
2290 let effective_id = effective.id();
2291
2292 if let Some(contribution) = effective
2294 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2295 .await
2296 {
2297 system_prompt_attributions.push(SystemPromptAttribution {
2298 capability_id: cap_id.to_string(),
2299 content: contribution.clone(),
2300 });
2301 system_prompt_parts.push(contribution);
2302 }
2303
2304 tools.extend(effective.tools_with_config(&cap_config.config));
2306 tool_definition_hooks
2307 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2308 tool_call_hooks.extend(effective.tool_call_hooks());
2309 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2311 let cap_category = effective.category();
2316 for def in effective.tool_definitions() {
2317 let def = match (def.category(), cap_category) {
2318 (None, Some(cat)) => def.with_category(cat),
2319 _ => def,
2320 }
2321 .with_capability_attribution(cap_id, Some(capability.name()));
2322 tool_definitions.push(def);
2323 }
2324
2325 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2333 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2334 {
2335 let threshold = cap_config
2337 .config
2338 .get("threshold")
2339 .and_then(|v| v.as_u64())
2340 .map(|v| v as usize)
2341 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2342 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2343 enabled: true,
2344 threshold,
2345 });
2346 }
2347
2348 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2349 let strategy = cap_config
2350 .config
2351 .get("strategy")
2352 .and_then(|v| v.as_str())
2353 .map(|value| match value {
2354 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2355 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2356 })
2357 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2358 let gemini_cached_content = cap_config
2359 .config
2360 .get("gemini_cached_content")
2361 .and_then(|v| v.as_str())
2362 .map(str::to_string);
2363 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2364 enabled: true,
2365 strategy,
2366 gemini_cached_content,
2367 });
2368 }
2369
2370 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2371 let server_tools =
2372 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2373 if !server_tools.is_empty() {
2374 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2375 server_tools,
2376 ..Default::default()
2377 });
2378 }
2379 }
2380
2381 mounts.extend(effective.mounts());
2383
2384 mcp_servers = merge_scoped_mcp_servers(
2385 &mcp_servers,
2386 &effective.mcp_servers_with_config(&cap_config.config),
2387 );
2388
2389 for skill in effective.contribute_skills() {
2393 mounts.push(skill.to_mount(cap_id));
2394 }
2395
2396 if let Some(provider) = effective.message_filter_provider() {
2398 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2399 message_filter_providers.push((provider, config));
2400 }
2401
2402 applied_ids.push(cap_id.to_string());
2403 }
2404 }
2405
2406 if !applied_ids
2418 .iter()
2419 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2420 && tool_definitions
2421 .iter()
2422 .any(|def| def.hints().supports_background == Some(true))
2423 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2424 && bg_cap.status() == CapabilityStatus::Available
2425 {
2426 tools.extend(bg_cap.tools());
2427 let cap_category = bg_cap.category();
2428 for def in bg_cap.tool_definitions() {
2429 let def = match (def.category(), cap_category) {
2430 (None, Some(cat)) => def.with_category(cat),
2431 _ => def,
2432 }
2433 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2434 tool_definitions.push(def);
2435 }
2436 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2437 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2438 }
2439
2440 tool_call_hooks.extend(narration_hooks);
2444
2445 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2447
2448 CollectedCapabilities {
2449 system_prompt_parts,
2450 system_prompt_attributions,
2451 tools,
2452 tool_definitions,
2453 mounts,
2454 message_filter_providers,
2455 applied_ids,
2456 tool_search,
2457 prompt_cache,
2458 openrouter_routing,
2459 tool_definition_hooks,
2460 tool_call_hooks,
2461 mcp_servers,
2462 }
2463}
2464
2465pub struct AppliedCapabilities {
2471 pub runtime_agent: RuntimeAgent,
2473 pub tool_registry: ToolRegistry,
2475 pub applied_ids: Vec<String>,
2477}
2478
2479pub async fn apply_capabilities(
2516 base_runtime_agent: RuntimeAgent,
2517 capability_ids: &[String],
2518 registry: &CapabilityRegistry,
2519 ctx: &SystemPromptContext,
2520) -> AppliedCapabilities {
2521 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2522
2523 let final_system_prompt = compose_system_prompt(
2525 &base_runtime_agent.system_prompt,
2526 collected.system_prompt_prefix().as_deref(),
2527 );
2528
2529 let mut tool_registry = ToolRegistry::new();
2531 for tool in collected.tools {
2532 tool_registry.register_boxed(tool);
2533 }
2534
2535 let mut tools = collected.tool_definitions;
2537 for hook in &collected.tool_definition_hooks {
2538 tools = hook.transform(tools);
2539 }
2540
2541 let runtime_agent = RuntimeAgent {
2542 system_prompt: final_system_prompt,
2543 model: base_runtime_agent.model,
2544 tools,
2545 max_iterations: base_runtime_agent.max_iterations,
2546 temperature: base_runtime_agent.temperature,
2547 max_tokens: base_runtime_agent.max_tokens,
2548 tool_search: collected.tool_search,
2549 prompt_cache: collected.prompt_cache,
2550 openrouter_routing: collected.openrouter_routing,
2551 network_access: base_runtime_agent.network_access,
2552 parallel_tool_calls: base_runtime_agent.parallel_tool_calls,
2553 };
2554
2555 AppliedCapabilities {
2556 runtime_agent,
2557 tool_registry,
2558 applied_ids: collected.applied_ids,
2559 }
2560}
2561
2562#[cfg(test)]
2567mod tests {
2568 use super::*;
2569 use crate::typed_id::SessionId;
2570 use std::collections::BTreeSet;
2571 use uuid::Uuid;
2572
2573 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2575
2576 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2577 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2578 }
2579
2580 fn test_ctx() -> SystemPromptContext {
2582 SystemPromptContext::without_file_store(SessionId::new())
2583 }
2584
2585 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2587 let mut ids = [
2588 "agent_instructions",
2589 "human_intent",
2590 "budgeting",
2591 "self_budget",
2592 "noop",
2593 "current_time",
2594 "research",
2595 "platform_management",
2596 "session_file_system",
2597 "session_storage",
2598 "session",
2599 "session_sql_database",
2600 "test_math",
2601 "test_weather",
2602 "stateless_todo_list",
2603 "web_fetch",
2604 "bashkit_shell",
2605 "background_execution",
2606 "session_schedule",
2607 "btw",
2608 "infinity_context",
2609 "compaction",
2610 "memory",
2611 "message_metadata",
2612 "openai_tool_search",
2613 "claude_tool_search",
2614 "tool_search",
2615 "auto_tool_search",
2616 "prompt_caching",
2617 "session_tasks",
2618 "skills",
2619 "subagents",
2620 "system_commands",
2621 "sample_data",
2622 "data_knowledge",
2623 "knowledge_base",
2624 "knowledge_index",
2625 "tool_output_persistence",
2626 "tool_output_distillation",
2627 "fake_warehouse",
2628 "fake_aws",
2629 "fake_crm",
2630 "fake_financial",
2631 "loop_detection",
2632 "tool_call_repair",
2633 "error_disclosure",
2634 "prompt_canary_guardrail",
2635 "guardrails",
2636 "user_hooks",
2637 "model_scout",
2638 "openrouter_workspace",
2639 "openrouter_server_tools",
2640 ]
2641 .into_iter()
2642 .collect::<BTreeSet<_>>();
2643 if cfg!(feature = "ui-capabilities") {
2644 ids.insert("openui");
2645 ids.insert("a2ui");
2646 }
2647 ids
2648 }
2649
2650 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2652 let mut ids = expected_core_builtin_ids();
2653 ids.insert("agent_handoff");
2654 ids.insert("a2a_agent_delegation");
2655 ids
2656 }
2657
2658 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2659 registry.capabilities.keys().map(String::as_str).collect()
2660 }
2661
2662 #[test]
2672 fn test_capability_registry_with_builtins_dev() {
2673 let _lock = lock_env();
2675 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2676 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2677 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2678 assert!(registry.has("agent_handoff"));
2679 assert!(registry.has("a2a_agent_delegation"));
2680 }
2681
2682 #[test]
2683 fn test_capability_registry_with_builtins_prod() {
2684 let _lock = lock_env();
2686 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2687 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2688 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2689 assert!(!registry.has("docker_container"));
2691 assert!(!registry.has("agent_handoff"));
2692 assert!(!registry.has("a2a_agent_delegation"));
2693 }
2694
2695 #[test]
2696 fn test_agent_delegation_enabled_by_env_in_prod() {
2697 let _lock = lock_env();
2699 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2700 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2701 assert!(registry.has("agent_handoff"));
2702 assert!(registry.has("a2a_agent_delegation"));
2703 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2704 }
2705
2706 #[test]
2707 fn test_agent_delegation_disabled_by_env_in_dev() {
2708 let _lock = lock_env();
2710 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2711 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2712 assert!(!registry.has("agent_handoff"));
2713 assert!(!registry.has("a2a_agent_delegation"));
2714 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2715 }
2716
2717 #[test]
2718 fn test_capability_registry_get() {
2719 let registry = CapabilityRegistry::with_builtins();
2720
2721 let noop = registry.get("noop").unwrap();
2722 assert_eq!(noop.id(), "noop");
2723 assert_eq!(noop.name(), "No-Op");
2724 assert_eq!(noop.status(), CapabilityStatus::Available);
2725 }
2726
2727 #[test]
2728 fn test_capability_registry_blueprint_with_capability() {
2729 struct BlueprintProviderCapability;
2730
2731 impl Capability for BlueprintProviderCapability {
2732 fn id(&self) -> &str {
2733 "blueprint_provider"
2734 }
2735 fn name(&self) -> &str {
2736 "Blueprint Provider"
2737 }
2738 fn description(&self) -> &str {
2739 "Capability that provides a blueprint for tests"
2740 }
2741 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2742 vec![AgentBlueprint {
2743 id: "test_blueprint",
2744 name: "Test Blueprint",
2745 description: "Blueprint for capability registry tests",
2746 model: BlueprintModel::Inherit,
2747 system_prompt: "Test prompt",
2748 tools: vec![],
2749 max_turns: None,
2750 config_schema: None,
2751 }]
2752 }
2753 }
2754
2755 let mut registry = CapabilityRegistry::new();
2756 registry.register(BlueprintProviderCapability);
2757
2758 let (capability_id, blueprint) = registry
2759 .blueprint_with_capability("test_blueprint")
2760 .expect("blueprint should resolve with capability id");
2761 assert_eq!(capability_id, "blueprint_provider");
2762 assert_eq!(blueprint.id, "test_blueprint");
2763 }
2764
2765 #[test]
2766 fn test_capability_registry_builder() {
2767 let registry = CapabilityRegistry::builder()
2768 .capability(NoopCapability)
2769 .capability(CurrentTimeCapability)
2770 .build();
2771
2772 assert!(registry.has("noop"));
2773 assert!(registry.has("current_time"));
2774 assert_eq!(registry.len(), 2);
2775 }
2776
2777 #[test]
2778 fn test_capability_status() {
2779 let registry = CapabilityRegistry::with_builtins();
2780
2781 let current_time = registry.get("current_time").unwrap();
2782 assert_eq!(current_time.status(), CapabilityStatus::Available);
2783
2784 let research = registry.get("research").unwrap();
2785 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2786 }
2787
2788 #[test]
2789 fn test_capability_icons_and_categories() {
2790 let registry = CapabilityRegistry::with_builtins();
2791
2792 let noop = registry.get("noop").unwrap();
2793 assert_eq!(noop.icon(), Some("circle-off"));
2794 assert_eq!(noop.category(), Some("Testing"));
2795
2796 let current_time = registry.get("current_time").unwrap();
2797 assert_eq!(current_time.icon(), Some("clock"));
2798 assert_eq!(current_time.category(), Some("Core"));
2799 }
2800
2801 #[test]
2802 fn test_system_prompt_preview_default_delegates_to_addition() {
2803 let registry = CapabilityRegistry::with_builtins();
2804
2805 let test_math = registry.get("test_math").unwrap();
2807 assert_eq!(
2808 test_math.system_prompt_preview().as_deref(),
2809 test_math.system_prompt_addition()
2810 );
2811
2812 let current_time = registry.get("current_time").unwrap();
2814 assert!(current_time.system_prompt_preview().is_none());
2815 assert!(current_time.system_prompt_addition().is_none());
2816 }
2817
2818 #[test]
2819 fn test_system_prompt_preview_dynamic_capability() {
2820 let registry = CapabilityRegistry::with_builtins();
2821 let cap = registry.get("agent_instructions").unwrap();
2822
2823 assert!(cap.system_prompt_addition().is_none());
2825 assert!(cap.system_prompt_preview().is_some());
2826 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2827 }
2828
2829 #[tokio::test]
2834 async fn test_apply_capabilities_empty() {
2835 let registry = CapabilityRegistry::with_builtins();
2836 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2837
2838 let applied =
2839 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
2840
2841 assert_eq!(
2842 applied.runtime_agent.system_prompt,
2843 base_runtime_agent.system_prompt
2844 );
2845 assert!(applied.tool_registry.is_empty());
2846 assert!(applied.applied_ids.is_empty());
2847 }
2848
2849 #[tokio::test]
2850 async fn test_apply_capabilities_noop() {
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(
2855 base_runtime_agent.clone(),
2856 &["noop".to_string()],
2857 ®istry,
2858 &test_ctx(),
2859 )
2860 .await;
2861
2862 assert_eq!(
2864 applied.runtime_agent.system_prompt,
2865 base_runtime_agent.system_prompt
2866 );
2867 assert!(applied.tool_registry.is_empty());
2868 assert_eq!(applied.applied_ids, vec!["noop"]);
2869 }
2870
2871 #[tokio::test]
2872 async fn test_apply_capabilities_current_time() {
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 &["current_time".to_string()],
2879 ®istry,
2880 &test_ctx(),
2881 )
2882 .await;
2883
2884 assert_eq!(
2886 applied.runtime_agent.system_prompt,
2887 base_runtime_agent.system_prompt
2888 );
2889 assert!(applied.tool_registry.has("get_current_time"));
2890 assert_eq!(applied.tool_registry.len(), 1);
2891 assert_eq!(applied.applied_ids, vec!["current_time"]);
2892 }
2893
2894 #[tokio::test]
2895 async fn test_apply_capabilities_skips_coming_soon() {
2896 let registry = CapabilityRegistry::with_builtins();
2897 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2898
2899 let applied = apply_capabilities(
2901 base_runtime_agent.clone(),
2902 &["research".to_string()],
2903 ®istry,
2904 &test_ctx(),
2905 )
2906 .await;
2907
2908 assert_eq!(
2910 applied.runtime_agent.system_prompt,
2911 base_runtime_agent.system_prompt
2912 );
2913 assert!(applied.applied_ids.is_empty()); }
2915
2916 #[tokio::test]
2917 async fn test_apply_capabilities_multiple() {
2918 let registry = CapabilityRegistry::with_builtins();
2919 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2920
2921 let applied = apply_capabilities(
2922 base_runtime_agent.clone(),
2923 &["noop".to_string(), "current_time".to_string()],
2924 ®istry,
2925 &test_ctx(),
2926 )
2927 .await;
2928
2929 assert!(applied.tool_registry.has("get_current_time"));
2930 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
2931 }
2932
2933 #[tokio::test]
2934 async fn test_apply_capabilities_preserves_order() {
2935 let registry = CapabilityRegistry::with_builtins();
2936 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
2937
2938 let applied = apply_capabilities(
2940 base_runtime_agent,
2941 &["current_time".to_string(), "noop".to_string()],
2942 ®istry,
2943 &test_ctx(),
2944 )
2945 .await;
2946
2947 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
2948 }
2949
2950 #[tokio::test]
2951 async fn test_apply_capabilities_test_math() {
2952 let registry = CapabilityRegistry::with_builtins();
2953 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2954
2955 let applied = apply_capabilities(
2956 base_runtime_agent.clone(),
2957 &["test_math".to_string()],
2958 ®istry,
2959 &test_ctx(),
2960 )
2961 .await;
2962
2963 assert!(
2965 !applied
2966 .runtime_agent
2967 .system_prompt
2968 .contains("<capability id=\"test_math\">")
2969 );
2970 assert!(
2972 applied
2973 .runtime_agent
2974 .system_prompt
2975 .contains("You are a helpful assistant.")
2976 );
2977 assert!(applied.tool_registry.has("add"));
2978 assert!(applied.tool_registry.has("subtract"));
2979 assert!(applied.tool_registry.has("multiply"));
2980 assert!(applied.tool_registry.has("divide"));
2981 assert_eq!(applied.tool_registry.len(), 4);
2982 }
2983
2984 #[tokio::test]
2985 async fn test_apply_capabilities_test_weather() {
2986 let registry = CapabilityRegistry::with_builtins();
2987 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2988
2989 let applied = apply_capabilities(
2990 base_runtime_agent.clone(),
2991 &["test_weather".to_string()],
2992 ®istry,
2993 &test_ctx(),
2994 )
2995 .await;
2996
2997 assert!(
2999 !applied
3000 .runtime_agent
3001 .system_prompt
3002 .contains("<capability id=\"test_weather\">")
3003 );
3004 assert!(applied.tool_registry.has("get_weather"));
3005 assert!(applied.tool_registry.has("get_forecast"));
3006 assert_eq!(applied.tool_registry.len(), 2);
3007 }
3008
3009 #[tokio::test]
3010 async fn test_apply_capabilities_test_math_and_test_weather() {
3011 let registry = CapabilityRegistry::with_builtins();
3012 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3013
3014 let applied = apply_capabilities(
3015 base_runtime_agent.clone(),
3016 &["test_math".to_string(), "test_weather".to_string()],
3017 ®istry,
3018 &test_ctx(),
3019 )
3020 .await;
3021
3022 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3025 assert!(applied.tool_registry.has("get_weather"));
3026 }
3027
3028 #[tokio::test]
3029 async fn test_apply_capabilities_stateless_todo_list() {
3030 let registry = CapabilityRegistry::with_builtins();
3031 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3032
3033 let applied = apply_capabilities(
3034 base_runtime_agent.clone(),
3035 &["stateless_todo_list".to_string()],
3036 ®istry,
3037 &test_ctx(),
3038 )
3039 .await;
3040
3041 assert!(
3043 applied
3044 .runtime_agent
3045 .system_prompt
3046 .contains("Task Management")
3047 );
3048 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3049 assert!(applied.tool_registry.has("write_todos"));
3050 assert_eq!(applied.tool_registry.len(), 1);
3051 }
3052
3053 #[tokio::test]
3054 async fn test_apply_capabilities_web_fetch() {
3055 let registry = CapabilityRegistry::with_builtins();
3056 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3057
3058 let applied = apply_capabilities(
3059 base_runtime_agent.clone(),
3060 &["web_fetch".to_string()],
3061 ®istry,
3062 &test_ctx(),
3063 )
3064 .await;
3065
3066 assert!(
3068 applied
3069 .runtime_agent
3070 .system_prompt
3071 .contains(&base_runtime_agent.system_prompt)
3072 );
3073 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3074 assert!(applied.tool_registry.has("web_fetch"));
3075 assert_eq!(applied.tool_registry.len(), 1);
3076 }
3077
3078 #[tokio::test]
3083 async fn test_xml_tags_wrap_capability_prompts() {
3084 let registry = CapabilityRegistry::with_builtins();
3085 let collected =
3086 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3087 .await;
3088
3089 assert_eq!(collected.system_prompt_parts.len(), 1);
3090 let part = &collected.system_prompt_parts[0];
3091 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3092 assert!(part.ends_with("</capability>"));
3093 assert!(part.contains("Task Management"));
3094 }
3095
3096 #[tokio::test]
3097 async fn test_xml_tags_multiple_capabilities() {
3098 let registry = CapabilityRegistry::with_builtins();
3099 let collected = collect_capabilities(
3100 &[
3101 "stateless_todo_list".to_string(),
3102 "session_schedule".to_string(),
3103 ],
3104 ®istry,
3105 &test_ctx(),
3106 )
3107 .await;
3108
3109 assert_eq!(collected.system_prompt_parts.len(), 2);
3110 assert!(
3111 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3112 );
3113 assert!(
3114 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3115 );
3116
3117 let prefix = collected.system_prompt_prefix().unwrap();
3118 assert!(prefix.contains("</capability>\n\n<capability"));
3120 }
3121
3122 #[tokio::test]
3123 async fn test_xml_tags_system_prompt_wrapping() {
3124 let registry = CapabilityRegistry::with_builtins();
3125 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3126
3127 let applied = apply_capabilities(
3128 base,
3129 &["stateless_todo_list".to_string()],
3130 ®istry,
3131 &test_ctx(),
3132 )
3133 .await;
3134
3135 let prompt = &applied.runtime_agent.system_prompt;
3136 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3137 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3139 assert!(prompt.contains("</capability>"));
3140 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3142 }
3143
3144 #[tokio::test]
3145 async fn test_no_xml_wrapping_without_capabilities() {
3146 let registry = CapabilityRegistry::with_builtins();
3147 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3148
3149 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3150
3151 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3153 assert!(
3154 !applied
3155 .runtime_agent
3156 .system_prompt
3157 .contains("<system-prompt>")
3158 );
3159 }
3160
3161 #[tokio::test]
3162 async fn test_no_xml_wrapping_for_noop_capability() {
3163 let registry = CapabilityRegistry::with_builtins();
3164 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3165
3166 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3168
3169 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3170 assert!(
3171 !applied
3172 .runtime_agent
3173 .system_prompt
3174 .contains("<system-prompt>")
3175 );
3176 }
3177
3178 #[tokio::test]
3183 async fn test_collect_capabilities_includes_mounts() {
3184 let registry = CapabilityRegistry::with_builtins();
3185
3186 let collected =
3187 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3188
3189 assert!(!collected.mounts.is_empty());
3190 assert_eq!(collected.mounts.len(), 1);
3191 assert_eq!(collected.mounts[0].path, "/samples");
3192 assert!(collected.mounts[0].is_readonly());
3193 }
3194
3195 #[tokio::test]
3196 async fn test_collect_capabilities_empty_mounts_by_default() {
3197 let registry = CapabilityRegistry::with_builtins();
3198
3199 let collected =
3201 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3202
3203 assert!(collected.mounts.is_empty());
3204 }
3205
3206 #[tokio::test]
3207 async fn test_collect_capabilities_combines_mounts() {
3208 let registry = CapabilityRegistry::with_builtins();
3209
3210 let collected = collect_capabilities(
3213 &["sample_data".to_string(), "current_time".to_string()],
3214 ®istry,
3215 &test_ctx(),
3216 )
3217 .await;
3218
3219 assert_eq!(collected.mounts.len(), 1);
3220 assert!(
3222 collected
3223 .applied_ids
3224 .iter()
3225 .any(|id| id == "session_file_system")
3226 );
3227 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3228 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3229 }
3230
3231 #[test]
3232 fn test_sample_data_capability() {
3233 let registry = CapabilityRegistry::with_builtins();
3234 let cap = registry.get("sample_data").unwrap();
3235
3236 assert_eq!(cap.id(), "sample_data");
3237 assert_eq!(cap.name(), "Sample Data");
3238 assert_eq!(cap.status(), CapabilityStatus::Available);
3239
3240 assert!(cap.system_prompt_addition().is_some());
3242 assert!(cap.tools().is_empty());
3243
3244 assert!(!cap.mounts().is_empty());
3246 }
3247
3248 #[test]
3253 fn test_resolve_dependencies_empty() {
3254 let registry = CapabilityRegistry::with_builtins();
3255
3256 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3257
3258 assert!(resolved.resolved_ids.is_empty());
3259 assert!(resolved.added_as_dependencies.is_empty());
3260 assert!(resolved.user_selected.is_empty());
3261 }
3262
3263 #[test]
3264 fn test_resolve_dependencies_no_deps() {
3265 let registry = CapabilityRegistry::with_builtins();
3266
3267 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3269
3270 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3271 assert!(resolved.added_as_dependencies.is_empty());
3272 }
3273
3274 #[test]
3275 fn test_resolve_dependencies_with_deps() {
3276 let registry = CapabilityRegistry::with_builtins();
3277
3278 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3280
3281 assert_eq!(resolved.resolved_ids.len(), 2);
3283 let fs_pos = resolved
3284 .resolved_ids
3285 .iter()
3286 .position(|id| id == "session_file_system")
3287 .unwrap();
3288 let sd_pos = resolved
3289 .resolved_ids
3290 .iter()
3291 .position(|id| id == "sample_data")
3292 .unwrap();
3293 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3294
3295 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3297 }
3298
3299 #[test]
3300 fn test_resolve_dependencies_already_selected() {
3301 let registry = CapabilityRegistry::with_builtins();
3302
3303 let resolved = resolve_dependencies(
3305 &["session_file_system".to_string(), "sample_data".to_string()],
3306 ®istry,
3307 )
3308 .unwrap();
3309
3310 assert_eq!(resolved.resolved_ids.len(), 2);
3311 assert!(resolved.added_as_dependencies.is_empty());
3313 }
3314
3315 #[test]
3316 fn test_resolve_dependencies_preserves_order() {
3317 let registry = CapabilityRegistry::with_builtins();
3318
3319 let resolved =
3321 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
3322 .unwrap();
3323
3324 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
3325 }
3326
3327 #[test]
3328 fn test_resolve_dependencies_unknown_capability() {
3329 let registry = CapabilityRegistry::with_builtins();
3330
3331 let resolved =
3333 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
3334
3335 assert!(resolved.resolved_ids.is_empty());
3336 }
3337
3338 #[test]
3339 fn test_get_dependencies() {
3340 let registry = CapabilityRegistry::with_builtins();
3341
3342 let deps = get_dependencies("sample_data", ®istry);
3344 assert_eq!(deps, vec!["session_file_system"]);
3345
3346 let deps = get_dependencies("current_time", ®istry);
3348 assert!(deps.is_empty());
3349
3350 let deps = get_dependencies("unknown", ®istry);
3352 assert!(deps.is_empty());
3353 }
3354
3355 #[test]
3356 fn test_sample_data_has_dependency() {
3357 let registry = CapabilityRegistry::with_builtins();
3358 let cap = registry.get("sample_data").unwrap();
3359
3360 let deps = cap.dependencies();
3361 assert_eq!(deps.len(), 1);
3362 assert_eq!(deps[0], "session_file_system");
3363 }
3364
3365 #[test]
3366 fn test_noop_has_no_dependencies() {
3367 let registry = CapabilityRegistry::with_builtins();
3368 let cap = registry.get("noop").unwrap();
3369
3370 assert!(cap.dependencies().is_empty());
3371 }
3372
3373 #[test]
3377 fn test_circular_dependency_error() {
3378 struct CapA;
3380 struct CapB;
3381
3382 impl Capability for CapA {
3383 fn id(&self) -> &str {
3384 "test_cap_a"
3385 }
3386 fn name(&self) -> &str {
3387 "Test A"
3388 }
3389 fn description(&self) -> &str {
3390 "Test capability A"
3391 }
3392 fn dependencies(&self) -> Vec<&'static str> {
3393 vec!["test_cap_b"]
3394 }
3395 }
3396
3397 impl Capability for CapB {
3398 fn id(&self) -> &str {
3399 "test_cap_b"
3400 }
3401 fn name(&self) -> &str {
3402 "Test B"
3403 }
3404 fn description(&self) -> &str {
3405 "Test capability B"
3406 }
3407 fn dependencies(&self) -> Vec<&'static str> {
3408 vec!["test_cap_a"]
3409 }
3410 }
3411
3412 let mut registry = CapabilityRegistry::new();
3413 registry.register(CapA);
3414 registry.register(CapB);
3415
3416 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
3417
3418 assert!(result.is_err());
3419 match result.unwrap_err() {
3420 DependencyError::CircularDependency { capability_id, .. } => {
3421 assert_eq!(capability_id, "test_cap_a");
3422 }
3423 _ => panic!("Expected CircularDependency error"),
3424 }
3425 }
3426
3427 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
3432
3433 struct FilterTestCapability {
3435 priority: i32,
3436 }
3437
3438 impl Capability for FilterTestCapability {
3439 fn id(&self) -> &str {
3440 "filter_test"
3441 }
3442 fn name(&self) -> &str {
3443 "Filter Test"
3444 }
3445 fn description(&self) -> &str {
3446 "Test capability with message filter"
3447 }
3448 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3449 Some(Arc::new(FilterTestProvider {
3450 priority: self.priority,
3451 }))
3452 }
3453 }
3454
3455 struct FilterTestProvider {
3456 priority: i32,
3457 }
3458
3459 impl MessageFilterProvider for FilterTestProvider {
3460 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
3461 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
3463 query
3464 .filters
3465 .push(MessageFilter::Search(search.to_string()));
3466 }
3467 }
3468
3469 fn priority(&self) -> i32 {
3470 self.priority
3471 }
3472 }
3473
3474 #[tokio::test]
3475 async fn test_collect_capabilities_with_configs_no_filter_providers() {
3476 let registry = CapabilityRegistry::with_builtins();
3477 let configs = vec![AgentCapabilityConfig {
3478 capability_ref: CapabilityId::new("current_time"),
3479 config: serde_json::json!({}),
3480 }];
3481
3482 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3483
3484 assert!(collected.message_filter_providers.is_empty());
3485 assert!(!collected.has_message_filters());
3486 }
3487
3488 #[tokio::test]
3489 async fn test_collect_capabilities_with_configs_with_filter_provider() {
3490 let mut registry = CapabilityRegistry::new();
3491 registry.register(FilterTestCapability { priority: 0 });
3492
3493 let configs = vec![AgentCapabilityConfig {
3494 capability_ref: CapabilityId::new("filter_test"),
3495 config: serde_json::json!({ "search": "hello" }),
3496 }];
3497
3498 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3499
3500 assert_eq!(collected.message_filter_providers.len(), 1);
3501 assert!(collected.has_message_filters());
3502 }
3503
3504 #[tokio::test]
3505 async fn test_collect_capabilities_with_configs_filter_priority_order() {
3506 struct HighPriorityCapability;
3508 struct LowPriorityCapability;
3509
3510 impl Capability for HighPriorityCapability {
3511 fn id(&self) -> &str {
3512 "high_priority"
3513 }
3514 fn name(&self) -> &str {
3515 "High Priority"
3516 }
3517 fn description(&self) -> &str {
3518 "Test"
3519 }
3520 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3521 Some(Arc::new(FilterTestProvider { priority: 10 }))
3522 }
3523 }
3524
3525 impl Capability for LowPriorityCapability {
3526 fn id(&self) -> &str {
3527 "low_priority"
3528 }
3529 fn name(&self) -> &str {
3530 "Low Priority"
3531 }
3532 fn description(&self) -> &str {
3533 "Test"
3534 }
3535 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3536 Some(Arc::new(FilterTestProvider { priority: -5 }))
3537 }
3538 }
3539
3540 let mut registry = CapabilityRegistry::new();
3541 registry.register(HighPriorityCapability);
3542 registry.register(LowPriorityCapability);
3543
3544 let configs = vec![
3546 AgentCapabilityConfig {
3547 capability_ref: CapabilityId::new("high_priority"),
3548 config: serde_json::json!({}),
3549 },
3550 AgentCapabilityConfig {
3551 capability_ref: CapabilityId::new("low_priority"),
3552 config: serde_json::json!({}),
3553 },
3554 ];
3555
3556 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3557
3558 assert_eq!(collected.message_filter_providers.len(), 2);
3560 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
3561 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
3562 }
3563
3564 #[tokio::test]
3565 async fn test_collected_capabilities_apply_message_filters() {
3566 let mut registry = CapabilityRegistry::new();
3567 registry.register(FilterTestCapability { priority: 0 });
3568
3569 let configs = vec![AgentCapabilityConfig {
3570 capability_ref: CapabilityId::new("filter_test"),
3571 config: serde_json::json!({ "search": "test_query" }),
3572 }];
3573
3574 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3575
3576 let session_id: SessionId = Uuid::now_v7().into();
3578 let mut query = MessageQuery::new(session_id);
3579
3580 collected.apply_message_filters(&mut query);
3581
3582 assert_eq!(query.filters.len(), 1);
3584 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3585 }
3586
3587 #[tokio::test]
3588 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3589 struct SearchCapability {
3590 id: &'static str,
3591 search_term: &'static str,
3592 priority: i32,
3593 }
3594
3595 struct SearchProvider {
3596 search_term: &'static str,
3597 priority: i32,
3598 }
3599
3600 impl MessageFilterProvider for SearchProvider {
3601 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3602 query
3603 .filters
3604 .push(MessageFilter::Search(self.search_term.to_string()));
3605 }
3606
3607 fn priority(&self) -> i32 {
3608 self.priority
3609 }
3610 }
3611
3612 impl Capability for SearchCapability {
3613 fn id(&self) -> &str {
3614 self.id
3615 }
3616 fn name(&self) -> &str {
3617 "Search"
3618 }
3619 fn description(&self) -> &str {
3620 "Test"
3621 }
3622 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3623 Some(Arc::new(SearchProvider {
3624 search_term: self.search_term,
3625 priority: self.priority,
3626 }))
3627 }
3628 }
3629
3630 let mut registry = CapabilityRegistry::new();
3631 registry.register(SearchCapability {
3632 id: "cap_a",
3633 search_term: "alpha",
3634 priority: 5,
3635 });
3636 registry.register(SearchCapability {
3637 id: "cap_b",
3638 search_term: "beta",
3639 priority: 1,
3640 });
3641 registry.register(SearchCapability {
3642 id: "cap_c",
3643 search_term: "gamma",
3644 priority: 10,
3645 });
3646
3647 let configs = vec![
3648 AgentCapabilityConfig {
3649 capability_ref: CapabilityId::new("cap_a"),
3650 config: serde_json::json!({}),
3651 },
3652 AgentCapabilityConfig {
3653 capability_ref: CapabilityId::new("cap_b"),
3654 config: serde_json::json!({}),
3655 },
3656 AgentCapabilityConfig {
3657 capability_ref: CapabilityId::new("cap_c"),
3658 config: serde_json::json!({}),
3659 },
3660 ];
3661
3662 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3663
3664 let session_id: SessionId = Uuid::now_v7().into();
3665 let mut query = MessageQuery::new(session_id);
3666
3667 collected.apply_message_filters(&mut query);
3668
3669 assert_eq!(query.filters.len(), 3);
3671 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3672 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3673 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3674 }
3675
3676 #[test]
3677 fn test_capability_without_message_filter_returns_none() {
3678 let registry = CapabilityRegistry::with_builtins();
3679
3680 let noop = registry.get("noop").unwrap();
3681 assert!(noop.message_filter_provider().is_none());
3682
3683 let current_time = registry.get("current_time").unwrap();
3684 assert!(current_time.message_filter_provider().is_none());
3685 }
3686
3687 #[tokio::test]
3688 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3689 let mut registry = CapabilityRegistry::new();
3690 registry.register(FilterTestCapability { priority: 0 });
3691
3692 let test_config = serde_json::json!({
3693 "search": "custom_search",
3694 "extra_field": 42
3695 });
3696
3697 let configs = vec![AgentCapabilityConfig {
3698 capability_ref: CapabilityId::new("filter_test"),
3699 config: test_config.clone(),
3700 }];
3701
3702 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3703
3704 assert_eq!(collected.message_filter_providers.len(), 1);
3706 let (_, stored_config) = &collected.message_filter_providers[0];
3707 assert_eq!(*stored_config, test_config);
3708 }
3709
3710 #[test]
3715 fn test_collect_message_filters_only_collects_filters() {
3716 let mut registry = CapabilityRegistry::new();
3717 registry.register(FilterTestCapability { priority: 0 });
3718
3719 let configs = vec![AgentCapabilityConfig {
3720 capability_ref: CapabilityId::new("filter_test"),
3721 config: serde_json::json!({ "search": "test_query" }),
3722 }];
3723
3724 let collected = collect_message_filters_only(&configs, ®istry);
3725
3726 let session_id: SessionId = Uuid::now_v7().into();
3727 let mut query = MessageQuery::new(session_id);
3728 collected.apply_message_filters(&mut query);
3729
3730 assert_eq!(query.filters.len(), 1);
3731 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3732 }
3733
3734 #[test]
3735 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
3736 let base = serde_json::json!({ "context_budget_tokens": 1000 });
3737
3738 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
3740 assert_eq!(with["compaction_active"], serde_json::json!(true));
3741 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
3742
3743 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
3744 assert!(without.get("compaction_active").is_none());
3745
3746 let other = message_filter_config_for("other", &base, true);
3748 assert!(other.get("compaction_active").is_none());
3749
3750 let null_base = message_filter_config_for(
3752 INFINITY_CONTEXT_CAPABILITY_ID,
3753 &serde_json::Value::Null,
3754 true,
3755 );
3756 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
3757 }
3758
3759 #[test]
3760 fn test_infinity_context_defers_to_compaction_end_to_end() {
3761 use crate::message::Message;
3762
3763 let mut registry = CapabilityRegistry::new();
3764 registry.register(InfinityContextCapability);
3765 registry.register(CompactionCapability);
3766
3767 let tight = serde_json::json!({
3768 "context_budget_tokens": 1,
3769 "min_recent_messages": 1
3770 });
3771
3772 let solo = vec![AgentCapabilityConfig {
3774 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3775 config: tight.clone(),
3776 }];
3777 let mut messages = vec![
3778 Message::user("task"),
3779 Message::assistant("old ".repeat(400)),
3780 Message::user("recent"),
3781 ];
3782 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
3783 assert!(
3784 messages
3785 .iter()
3786 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
3787 "infinity context alone should trim and notice"
3788 );
3789
3790 let both = vec![
3792 AgentCapabilityConfig {
3793 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3794 config: tight,
3795 },
3796 AgentCapabilityConfig {
3797 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3798 config: serde_json::json!({}),
3799 },
3800 ];
3801 let mut messages = vec![
3802 Message::user("task"),
3803 Message::assistant("old ".repeat(400)),
3804 Message::user("recent"),
3805 ];
3806 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
3807 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
3808 assert!(
3809 messages
3810 .iter()
3811 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
3812 "no hidden-history notice when compaction is the active reducer"
3813 );
3814 }
3815
3816 #[test]
3817 fn test_compaction_is_enabled_detects_compaction() {
3818 let mut registry = CapabilityRegistry::new();
3819 registry.register(CompactionCapability);
3820
3821 let with_compaction = vec![AgentCapabilityConfig {
3822 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3823 config: serde_json::json!({}),
3824 }];
3825 assert!(compaction_is_enabled(&with_compaction, ®istry));
3826
3827 let without = vec![AgentCapabilityConfig {
3828 capability_ref: CapabilityId::new("current_time"),
3829 config: serde_json::json!({}),
3830 }];
3831 assert!(!compaction_is_enabled(&without, ®istry));
3832 }
3833
3834 #[test]
3835 fn test_collect_message_filters_only_skips_unknown_capabilities() {
3836 let registry = CapabilityRegistry::new();
3837
3838 let configs = vec![AgentCapabilityConfig {
3839 capability_ref: CapabilityId::new("nonexistent"),
3840 config: serde_json::json!({}),
3841 }];
3842
3843 let collected = collect_message_filters_only(&configs, ®istry);
3844 assert!(collected.message_filter_providers.is_empty());
3845 }
3846
3847 #[test]
3848 fn test_collect_message_filters_only_preserves_priority_order() {
3849 struct PriorityFilterCap {
3850 id: &'static str,
3851 search_term: &'static str,
3852 priority: i32,
3853 }
3854
3855 struct PriorityFilterProvider {
3856 search_term: &'static str,
3857 priority: i32,
3858 }
3859
3860 impl Capability for PriorityFilterCap {
3861 fn id(&self) -> &str {
3862 self.id
3863 }
3864 fn name(&self) -> &str {
3865 self.id
3866 }
3867 fn description(&self) -> &str {
3868 "priority test"
3869 }
3870 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3871 Some(Arc::new(PriorityFilterProvider {
3872 search_term: self.search_term,
3873 priority: self.priority,
3874 }))
3875 }
3876 }
3877
3878 impl MessageFilterProvider for PriorityFilterProvider {
3879 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3880 query
3881 .filters
3882 .push(MessageFilter::Search(self.search_term.to_string()));
3883 }
3884 fn priority(&self) -> i32 {
3885 self.priority
3886 }
3887 }
3888
3889 let mut registry = CapabilityRegistry::new();
3890 registry.register(PriorityFilterCap {
3891 id: "gamma",
3892 search_term: "gamma",
3893 priority: 10,
3894 });
3895 registry.register(PriorityFilterCap {
3896 id: "alpha",
3897 search_term: "alpha",
3898 priority: 5,
3899 });
3900 registry.register(PriorityFilterCap {
3901 id: "beta",
3902 search_term: "beta",
3903 priority: 1,
3904 });
3905
3906 let configs = vec![
3907 AgentCapabilityConfig {
3908 capability_ref: CapabilityId::new("gamma"),
3909 config: serde_json::json!({}),
3910 },
3911 AgentCapabilityConfig {
3912 capability_ref: CapabilityId::new("alpha"),
3913 config: serde_json::json!({}),
3914 },
3915 AgentCapabilityConfig {
3916 capability_ref: CapabilityId::new("beta"),
3917 config: serde_json::json!({}),
3918 },
3919 ];
3920
3921 let collected = collect_message_filters_only(&configs, ®istry);
3922
3923 let session_id: SessionId = Uuid::now_v7().into();
3924 let mut query = MessageQuery::new(session_id);
3925 collected.apply_message_filters(&mut query);
3926
3927 assert_eq!(query.filters.len(), 3);
3929 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3930 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3931 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3932 }
3933
3934 #[test]
3935 fn test_collect_message_filters_only_post_load_invoked() {
3936 use crate::message::Message;
3937
3938 struct PostLoadCap;
3939 struct PostLoadProvider;
3940
3941 impl Capability for PostLoadCap {
3942 fn id(&self) -> &str {
3943 "post_load_test"
3944 }
3945 fn name(&self) -> &str {
3946 "PostLoad Test"
3947 }
3948 fn description(&self) -> &str {
3949 "test"
3950 }
3951 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3952 Some(Arc::new(PostLoadProvider))
3953 }
3954 }
3955
3956 impl MessageFilterProvider for PostLoadProvider {
3957 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
3958 fn priority(&self) -> i32 {
3959 0
3960 }
3961 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
3962 messages.reverse();
3964 }
3965 }
3966
3967 let mut registry = CapabilityRegistry::new();
3968 registry.register(PostLoadCap);
3969
3970 let configs = vec![AgentCapabilityConfig {
3971 capability_ref: CapabilityId::new("post_load_test"),
3972 config: serde_json::json!({}),
3973 }];
3974
3975 let collected = collect_message_filters_only(&configs, ®istry);
3976
3977 let mut messages = vec![Message::user("first"), Message::user("second")];
3978 collected.apply_post_load_filters(&mut messages);
3979
3980 assert_eq!(messages[0].text(), Some("second"));
3982 assert_eq!(messages[1].text(), Some("first"));
3983 }
3984
3985 #[test]
3986 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
3987 use crate::tool_types::ToolCall;
3988
3989 fn tool_heavy_messages() -> Vec<Message> {
3990 let mut messages = vec![Message::user("inspect files repeatedly")];
3991 for index in 0..9 {
3992 let call_id = format!("call_{index}");
3993 messages.push(Message::assistant_with_tools(
3994 "",
3995 vec![ToolCall {
3996 id: call_id.clone(),
3997 name: "read_file".to_string(),
3998 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
3999 }],
4000 ));
4001 messages.push(Message::tool_result(
4002 call_id,
4003 Some(serde_json::json!({
4004 "path": "/workspace/src/lib.rs",
4005 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4006 "total_lines": 1000,
4007 "lines_shown": {"start": 1, "end": 1000},
4008 "truncated": false
4009 })),
4010 None,
4011 ));
4012 }
4013 messages
4014 }
4015
4016 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4017 messages[2]
4018 .tool_result_content()
4019 .and_then(|result| result.result.as_ref())
4020 .and_then(|result| result.get("masked"))
4021 .and_then(|masked| masked.as_bool())
4022 .unwrap_or(false)
4023 }
4024
4025 let mut registry = CapabilityRegistry::new();
4026 registry.register(CompactionCapability);
4027 let context = ModelViewContext {
4028 session_id: SessionId::new(),
4029 prior_usage: None,
4030 };
4031
4032 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4033 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4034 assert!(!first_tool_result_is_masked(&unmasked));
4035
4036 let compaction = collect_model_view_providers(
4037 &[AgentCapabilityConfig {
4038 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4039 config: serde_json::json!({}),
4040 }],
4041 ®istry,
4042 None,
4043 );
4044 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4045 assert!(first_tool_result_is_masked(&masked));
4046 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4047 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4048 }
4049
4050 struct DelegatingFilterCap {
4053 id: &'static str,
4054 inner: std::sync::Arc<InnerFilterCap>,
4055 }
4056 struct InnerFilterCap;
4057
4058 impl Capability for InnerFilterCap {
4059 fn id(&self) -> &str {
4060 "inner_filter"
4061 }
4062 fn name(&self) -> &str {
4063 "Inner Filter"
4064 }
4065 fn description(&self) -> &str {
4066 "inner"
4067 }
4068 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4069 Some(std::sync::Arc::new(SentinelFilter))
4070 }
4071 }
4072 struct SentinelFilter;
4073 impl MessageFilterProvider for SentinelFilter {
4074 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4075 }
4076 impl Capability for DelegatingFilterCap {
4077 fn id(&self) -> &str {
4078 self.id
4079 }
4080 fn name(&self) -> &str {
4081 "Delegating Filter"
4082 }
4083 fn description(&self) -> &str {
4084 "delegating"
4085 }
4086 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4087 None }
4089 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4090 Some(&*self.inner)
4091 }
4092 }
4093
4094 #[test]
4095 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4096 let inner = std::sync::Arc::new(InnerFilterCap);
4097 let outer = DelegatingFilterCap {
4098 id: "delegating_filter",
4099 inner: inner.clone(),
4100 };
4101
4102 let mut registry = CapabilityRegistry::new();
4103 registry.register(outer);
4104
4105 let configs = vec![AgentCapabilityConfig {
4106 capability_ref: CapabilityId::new("delegating_filter"),
4107 config: serde_json::json!({}),
4108 }];
4109
4110 let collected = collect_message_filters_only(&configs, ®istry);
4113 assert_eq!(
4114 collected.message_filter_providers.len(),
4115 1,
4116 "provider from resolved inner capability must be collected"
4117 );
4118 }
4119
4120 struct DelegatingMvpCap {
4121 id: &'static str,
4122 inner: std::sync::Arc<InnerMvpCap>,
4123 }
4124 struct InnerMvpCap;
4125
4126 impl Capability for InnerMvpCap {
4127 fn id(&self) -> &str {
4128 "inner_mvp"
4129 }
4130 fn name(&self) -> &str {
4131 "Inner MVP"
4132 }
4133 fn description(&self) -> &str {
4134 "inner"
4135 }
4136 fn model_view_provider(
4137 &self,
4138 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4139 struct NoopMvp;
4141 impl crate::capabilities::ModelViewProvider for NoopMvp {
4142 fn apply_model_view(
4143 &self,
4144 messages: Vec<Message>,
4145 _config: &serde_json::Value,
4146 _context: &ModelViewContext<'_>,
4147 ) -> Vec<Message> {
4148 messages
4149 }
4150 }
4151 Some(std::sync::Arc::new(NoopMvp))
4152 }
4153 }
4154 impl Capability for DelegatingMvpCap {
4155 fn id(&self) -> &str {
4156 self.id
4157 }
4158 fn name(&self) -> &str {
4159 "Delegating MVP"
4160 }
4161 fn description(&self) -> &str {
4162 "delegating"
4163 }
4164 fn model_view_provider(
4165 &self,
4166 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4167 None }
4169 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4170 Some(&*self.inner)
4171 }
4172 }
4173
4174 #[test]
4175 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4176 let inner = std::sync::Arc::new(InnerMvpCap);
4177 let outer = DelegatingMvpCap {
4178 id: "delegating_mvp",
4179 inner: inner.clone(),
4180 };
4181
4182 let mut registry = CapabilityRegistry::new();
4183 registry.register(outer);
4184
4185 let configs = vec![AgentCapabilityConfig {
4186 capability_ref: CapabilityId::new("delegating_mvp"),
4187 config: serde_json::json!({}),
4188 }];
4189
4190 let collected = collect_model_view_providers(&configs, ®istry, None);
4193 assert_eq!(
4194 collected.model_view_providers.len(),
4195 1,
4196 "provider from resolved inner capability must be collected"
4197 );
4198 }
4199
4200 #[tokio::test]
4210 async fn test_bashkit_shell_capability_produces_bash_tool() {
4211 let registry = CapabilityRegistry::with_builtins();
4212 let collected =
4213 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4214
4215 let tool_names: Vec<&str> = collected
4216 .tool_definitions
4217 .iter()
4218 .map(|t| t.name())
4219 .collect();
4220 assert!(
4221 tool_names.contains(&"bash"),
4222 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4223 tool_names
4224 );
4225 assert!(
4226 !collected.tools.is_empty(),
4227 "bashkit_shell must provide tool implementations"
4228 );
4229 }
4230
4231 #[tokio::test]
4232 async fn test_generic_harness_capability_set_produces_bash_tool() {
4233 let generic_harness_caps = vec![
4236 "session_file_system".to_string(),
4237 "bashkit_shell".to_string(),
4238 "web_fetch".to_string(),
4239 "session_storage".to_string(),
4240 "session".to_string(),
4241 "agent_instructions".to_string(),
4242 "skills".to_string(),
4243 "infinity_context".to_string(),
4244 "auto_tool_search".to_string(),
4245 ];
4246
4247 let registry = CapabilityRegistry::with_builtins();
4248 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4249
4250 let tool_names: Vec<&str> = collected
4251 .tool_definitions
4252 .iter()
4253 .map(|t| t.name())
4254 .collect();
4255 assert!(
4256 tool_names.contains(&"bash"),
4257 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4258 tool_names
4259 );
4260 }
4261
4262 #[tokio::test]
4263 async fn test_collect_capabilities_tool_count_matches_definitions() {
4264 let registry = CapabilityRegistry::with_builtins();
4267 let collected =
4268 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4269
4270 assert_eq!(
4271 collected.tools.len(),
4272 collected.tool_definitions.len(),
4273 "tool implementations ({}) must match tool definitions ({})",
4274 collected.tools.len(),
4275 collected.tool_definitions.len(),
4276 );
4277 }
4278
4279 #[tokio::test]
4283 async fn test_collect_capabilities_resolves_dependencies() {
4284 let registry = CapabilityRegistry::with_builtins();
4287 let collected =
4288 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4289
4290 assert!(
4292 collected
4293 .applied_ids
4294 .iter()
4295 .any(|id| id == "session_file_system"),
4296 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4297 collected.applied_ids
4298 );
4299
4300 let tool_names: Vec<&str> = collected
4301 .tool_definitions
4302 .iter()
4303 .map(|t| t.name())
4304 .collect();
4305
4306 assert!(
4308 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4309 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
4310 tool_names
4311 );
4312
4313 assert_eq!(
4315 collected.tools.len(),
4316 collected.tool_definitions.len(),
4317 "dependency-added tools must have implementations, not just definitions"
4318 );
4319 }
4320
4321 #[test]
4322 fn test_defaults_do_not_include_bash() {
4323 let registry = crate::ToolRegistry::with_defaults();
4326 assert!(
4327 !registry.has("bash"),
4328 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
4329 );
4330 }
4331
4332 #[tokio::test]
4339 async fn test_background_execution_auto_activates_with_bashkit_shell() {
4340 let registry = CapabilityRegistry::with_builtins();
4341 let collected =
4342 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4343
4344 let tool_names: Vec<&str> = collected
4345 .tool_definitions
4346 .iter()
4347 .map(|t| t.name())
4348 .collect();
4349 assert!(
4350 tool_names.contains(&"spawn_background"),
4351 "spawn_background must be auto-activated when bashkit_shell (a \
4352 background-capable tool) is in the agent's capability set; got: {:?}",
4353 tool_names
4354 );
4355 assert!(
4356 collected
4357 .applied_ids
4358 .iter()
4359 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4360 "background_execution must be in applied_ids when auto-activated; \
4361 got: {:?}",
4362 collected.applied_ids
4363 );
4364
4365 assert!(
4367 collected
4368 .tools
4369 .iter()
4370 .any(|t| t.name() == "spawn_background"),
4371 "spawn_background tool implementation must be present alongside the \
4372 definition (lockstep contract)"
4373 );
4374 }
4375
4376 #[tokio::test]
4379 async fn test_background_execution_does_not_auto_activate_without_hint() {
4380 let registry = CapabilityRegistry::with_builtins();
4381 let collected =
4383 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4384
4385 let tool_names: Vec<&str> = collected
4386 .tool_definitions
4387 .iter()
4388 .map(|t| t.name())
4389 .collect();
4390 assert!(
4391 !tool_names.contains(&"spawn_background"),
4392 "spawn_background must NOT be activated without a background-capable \
4393 tool; got: {:?}",
4394 tool_names
4395 );
4396 assert!(
4397 !collected
4398 .applied_ids
4399 .iter()
4400 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4401 "background_execution must not appear in applied_ids when no \
4402 background-capable tool is present; got: {:?}",
4403 collected.applied_ids
4404 );
4405 }
4406
4407 #[tokio::test]
4411 async fn test_background_execution_explicit_selection_is_idempotent() {
4412 let registry = CapabilityRegistry::with_builtins();
4413 let collected = collect_capabilities(
4414 &[
4415 "bashkit_shell".to_string(),
4416 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
4417 ],
4418 ®istry,
4419 &test_ctx(),
4420 )
4421 .await;
4422
4423 let spawn_background_count = collected
4424 .tool_definitions
4425 .iter()
4426 .filter(|t| t.name() == "spawn_background")
4427 .count();
4428 assert_eq!(
4429 spawn_background_count, 1,
4430 "spawn_background must appear exactly once even when \
4431 background_execution is selected explicitly alongside a \
4432 background-capable tool"
4433 );
4434 let applied_count = collected
4435 .applied_ids
4436 .iter()
4437 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
4438 .count();
4439 assert_eq!(
4440 applied_count, 1,
4441 "background_execution must appear exactly once in applied_ids"
4442 );
4443 }
4444
4445 #[test]
4450 fn test_defaults_do_not_include_spawn_background() {
4451 let registry = crate::ToolRegistry::with_defaults();
4452 assert!(
4453 !registry.has("spawn_background"),
4454 "with_defaults() must not include 'spawn_background' — it comes \
4455 from the background_execution capability (EVE-501)"
4456 );
4457 }
4458
4459 #[test]
4464 fn test_capability_features_default_empty() {
4465 let registry = CapabilityRegistry::with_builtins();
4466
4467 let noop = registry.get("noop").unwrap();
4469 assert!(noop.features().is_empty());
4470
4471 let current_time = registry.get("current_time").unwrap();
4472 assert!(current_time.features().is_empty());
4473 }
4474
4475 #[test]
4476 fn test_file_system_capability_features() {
4477 let registry = CapabilityRegistry::with_builtins();
4478
4479 let fs = registry.get("session_file_system").unwrap();
4480 assert_eq!(fs.features(), vec!["file_system"]);
4481 }
4482
4483 #[test]
4484 fn test_bashkit_shell_capability_features() {
4485 let registry = CapabilityRegistry::with_builtins();
4486
4487 let bash = registry.get("bashkit_shell").unwrap();
4488 assert_eq!(bash.features(), vec!["file_system"]);
4489 }
4490
4491 #[test]
4492 fn test_alias_resolves_to_canonical_capability() {
4493 let registry = CapabilityRegistry::with_builtins();
4494
4495 let via_alias = registry.get("virtual_bash").unwrap();
4497 assert_eq!(via_alias.id(), "bashkit_shell");
4498 assert!(registry.has("virtual_bash"));
4499 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
4500 assert_eq!(
4501 registry.canonical_id("bashkit_shell"),
4502 Some("bashkit_shell")
4503 );
4504 assert_eq!(registry.canonical_id("nonexistent"), None);
4505 }
4506
4507 #[test]
4508 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
4509 let registry = CapabilityRegistry::with_builtins();
4510
4511 let resolved = resolve_dependencies(
4514 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
4515 ®istry,
4516 )
4517 .unwrap();
4518 let bash_ids: Vec<_> = resolved
4519 .resolved_ids
4520 .iter()
4521 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
4522 .collect();
4523 assert_eq!(bash_ids, vec!["bashkit_shell"]);
4524 assert!(
4526 !resolved
4527 .added_as_dependencies
4528 .contains(&"bashkit_shell".to_string())
4529 );
4530 }
4531
4532 #[test]
4533 fn test_alias_preserves_explicit_config_in_resolution() {
4534 let registry = CapabilityRegistry::with_builtins();
4535
4536 let configs = vec![AgentCapabilityConfig::with_config(
4537 "virtual_bash".to_string(),
4538 serde_json::json!({"key": "value"}),
4539 )];
4540 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
4541 let bash = resolved
4542 .iter()
4543 .find(|c| c.capability_id() == "bashkit_shell")
4544 .expect("alias must resolve to canonical bashkit_shell config");
4545 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
4546 }
4547
4548 #[test]
4549 fn test_unregister_by_alias_removes_capability_and_aliases() {
4550 let mut registry = CapabilityRegistry::with_builtins();
4551
4552 assert!(registry.unregister("virtual_bash").is_some());
4553 assert!(!registry.has("bashkit_shell"));
4554 assert!(!registry.has("virtual_bash"));
4555 }
4556
4557 #[test]
4558 fn test_session_storage_capability_features() {
4559 let registry = CapabilityRegistry::with_builtins();
4560
4561 let storage = registry.get("session_storage").unwrap();
4562 let features = storage.features();
4563 assert!(features.contains(&"secrets"));
4564 assert!(features.contains(&"key_value"));
4565 }
4566
4567 #[test]
4568 fn test_session_schedule_capability_features() {
4569 let registry = CapabilityRegistry::with_builtins();
4570
4571 let schedule = registry.get("session_schedule").unwrap();
4572 assert_eq!(schedule.features(), vec!["schedules"]);
4573 }
4574
4575 #[test]
4576 fn test_session_sql_database_capability_features() {
4577 let registry = CapabilityRegistry::with_builtins();
4578
4579 let sql = registry.get("session_sql_database").unwrap();
4580 assert_eq!(sql.features(), vec!["sql_database"]);
4581 }
4582
4583 #[test]
4584 fn test_sample_data_capability_features() {
4585 let registry = CapabilityRegistry::with_builtins();
4586
4587 let sample = registry.get("sample_data").unwrap();
4588 assert_eq!(sample.features(), vec!["file_system"]);
4589 }
4590
4591 #[test]
4592 fn test_compute_features_empty() {
4593 let registry = CapabilityRegistry::with_builtins();
4594
4595 let features = compute_features(&[], ®istry);
4596 assert!(features.is_empty());
4597 }
4598
4599 #[test]
4600 fn test_compute_features_single_capability() {
4601 let registry = CapabilityRegistry::with_builtins();
4602
4603 let features = compute_features(&["session_schedule".to_string()], ®istry);
4604 assert_eq!(features, vec!["schedules"]);
4605 }
4606
4607 #[test]
4608 fn test_compute_features_multiple_capabilities() {
4609 let registry = CapabilityRegistry::with_builtins();
4610
4611 let features = compute_features(
4612 &[
4613 "session_file_system".to_string(),
4614 "session_storage".to_string(),
4615 "session_schedule".to_string(),
4616 ],
4617 ®istry,
4618 );
4619 assert!(features.contains(&"file_system".to_string()));
4620 assert!(features.contains(&"secrets".to_string()));
4621 assert!(features.contains(&"key_value".to_string()));
4622 assert!(features.contains(&"schedules".to_string()));
4623 }
4624
4625 #[test]
4626 fn test_compute_features_deduplicates() {
4627 let registry = CapabilityRegistry::with_builtins();
4628
4629 let features = compute_features(
4631 &[
4632 "session_file_system".to_string(),
4633 "bashkit_shell".to_string(),
4634 ],
4635 ®istry,
4636 );
4637 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
4638 assert_eq!(file_system_count, 1, "file_system should appear only once");
4639 }
4640
4641 #[test]
4642 fn test_compute_features_includes_dependency_features() {
4643 let registry = CapabilityRegistry::with_builtins();
4644
4645 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
4647 assert!(features.contains(&"file_system".to_string()));
4648 }
4649
4650 #[test]
4651 fn test_compute_features_generic_harness_set() {
4652 let registry = CapabilityRegistry::with_builtins();
4653
4654 let features = compute_features(
4656 &[
4657 "session_file_system".to_string(),
4658 "bashkit_shell".to_string(),
4659 "session_storage".to_string(),
4660 "session".to_string(),
4661 "session_schedule".to_string(),
4662 ],
4663 ®istry,
4664 );
4665 assert!(features.contains(&"file_system".to_string()));
4666 assert!(features.contains(&"secrets".to_string()));
4667 assert!(features.contains(&"key_value".to_string()));
4668 assert!(features.contains(&"schedules".to_string()));
4669 }
4670
4671 #[test]
4672 fn test_compute_features_unknown_capability_ignored() {
4673 let registry = CapabilityRegistry::with_builtins();
4674
4675 let features = compute_features(
4676 &["unknown_cap".to_string(), "session_schedule".to_string()],
4677 ®istry,
4678 );
4679 assert_eq!(features, vec!["schedules"]);
4680 }
4681
4682 #[test]
4683 fn test_risk_level_ordering() {
4684 assert!(RiskLevel::Low < RiskLevel::Medium);
4685 assert!(RiskLevel::Medium < RiskLevel::High);
4686 }
4687
4688 #[test]
4689 fn test_risk_level_serde_roundtrip() {
4690 let high = RiskLevel::High;
4691 let json = serde_json::to_string(&high).unwrap();
4692 assert_eq!(json, "\"high\"");
4693 let back: RiskLevel = serde_json::from_str(&json).unwrap();
4694 assert_eq!(back, RiskLevel::High);
4695 }
4696
4697 #[test]
4698 fn test_capability_risk_levels() {
4699 let registry = CapabilityRegistry::with_builtins();
4700
4701 let bash = registry.get("bashkit_shell").unwrap();
4703 assert_eq!(bash.risk_level(), RiskLevel::High);
4704
4705 let fetch = registry.get("web_fetch").unwrap();
4707 assert_eq!(fetch.risk_level(), RiskLevel::High);
4708
4709 let noop = registry.get("noop").unwrap();
4711 assert_eq!(noop.risk_level(), RiskLevel::Low);
4712 }
4713
4714 #[tokio::test]
4719 async fn test_apply_capabilities_openai_tool_search() {
4720 let registry = CapabilityRegistry::with_builtins();
4721 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4722
4723 let applied = apply_capabilities(
4724 base_runtime_agent.clone(),
4725 &["openai_tool_search".to_string()],
4726 ®istry,
4727 &test_ctx(),
4728 )
4729 .await;
4730
4731 assert_eq!(
4733 applied.runtime_agent.system_prompt,
4734 base_runtime_agent.system_prompt
4735 );
4736 assert!(applied.tool_registry.is_empty());
4737 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
4738
4739 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4741 assert!(ts.enabled);
4742 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4743 }
4744
4745 #[tokio::test]
4746 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
4747 let registry = CapabilityRegistry::with_builtins();
4748 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4749
4750 let applied = apply_capabilities(
4751 base_runtime_agent,
4752 &[
4753 "current_time".to_string(),
4754 "openai_tool_search".to_string(),
4755 "test_math".to_string(),
4756 ],
4757 ®istry,
4758 &test_ctx(),
4759 )
4760 .await;
4761
4762 assert!(applied.tool_registry.has("get_current_time"));
4764 assert!(applied.tool_registry.has("add"));
4765 assert!(applied.tool_registry.has("subtract"));
4766 assert!(applied.tool_registry.has("multiply"));
4767 assert!(applied.tool_registry.has("divide"));
4768
4769 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4771 assert!(ts.enabled);
4772 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4773 }
4774
4775 #[tokio::test]
4776 async fn test_collect_capabilities_tool_search_custom_threshold() {
4777 let registry = CapabilityRegistry::with_builtins();
4778
4779 let configs = vec![AgentCapabilityConfig {
4780 capability_ref: CapabilityId::new("openai_tool_search"),
4781 config: serde_json::json!({"threshold": 5}),
4782 }];
4783
4784 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4785
4786 let ts = collected.tool_search.as_ref().unwrap();
4787 assert!(ts.enabled);
4788 assert_eq!(ts.threshold, 5);
4789 }
4790
4791 #[tokio::test]
4792 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
4793 let registry = CapabilityRegistry::with_builtins();
4794
4795 let configs = vec![
4796 AgentCapabilityConfig {
4797 capability_ref: CapabilityId::new("auto_tool_search"),
4798 config: serde_json::json!({"threshold": 2}),
4799 },
4800 AgentCapabilityConfig {
4801 capability_ref: CapabilityId::new("test_math"),
4802 config: serde_json::json!({}),
4803 },
4804 ];
4805
4806 let ctx = test_ctx().with_model("claude-3-5-haiku");
4810 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4811
4812 assert!(
4813 collected.tool_search.is_none(),
4814 "auto_tool_search must not set a hosted config on a non-native model"
4815 );
4816 assert!(
4817 collected
4818 .tools
4819 .iter()
4820 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4821 "auto_tool_search must contribute the client-side tool_search tool"
4822 );
4823 assert!(
4824 !collected.tool_definition_hooks.is_empty(),
4825 "auto_tool_search must contribute a client-side deferral hook"
4826 );
4827
4828 let mut transformed = collected.tool_definitions.clone();
4829 for hook in &collected.tool_definition_hooks {
4830 transformed = hook.transform(transformed);
4831 }
4832 let add_tool = transformed
4833 .iter()
4834 .find(|tool| tool.name() == "add")
4835 .expect("test_math contributes add");
4836 assert!(
4837 add_tool.parameters().get("properties").is_none(),
4838 "generic auto_tool_search must honor the configured threshold"
4839 );
4840 }
4841
4842 #[tokio::test]
4843 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
4844 let registry = CapabilityRegistry::with_builtins();
4845
4846 let configs = vec![AgentCapabilityConfig {
4847 capability_ref: CapabilityId::new("auto_tool_search"),
4848 config: serde_json::json!({"threshold": 7}),
4849 }];
4850
4851 let ctx = test_ctx().with_model("gpt-5.4");
4854 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4855
4856 let ts = collected
4857 .tool_search
4858 .as_ref()
4859 .expect("auto_tool_search must set a hosted config on a native model");
4860 assert!(ts.enabled);
4861 assert_eq!(ts.threshold, 7);
4862 assert!(
4863 !collected
4864 .tools
4865 .iter()
4866 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4867 "hosted mechanism must not contribute the client-side tool_search tool"
4868 );
4869 assert!(
4870 collected.tool_definition_hooks.is_empty(),
4871 "hosted mechanism must not contribute a client-side deferral hook"
4872 );
4873 }
4874
4875 #[tokio::test]
4876 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
4877 let registry = CapabilityRegistry::with_builtins();
4878
4879 let configs = vec![AgentCapabilityConfig {
4880 capability_ref: CapabilityId::new("auto_tool_search"),
4881 config: serde_json::json!({"threshold": 9}),
4882 }];
4883
4884 let ctx = test_ctx().with_model("claude-opus-4-8");
4887 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4888
4889 let ts = collected
4890 .tool_search
4891 .as_ref()
4892 .expect("auto_tool_search must set a hosted config on a native Claude model");
4893 assert!(ts.enabled);
4894 assert_eq!(ts.threshold, 9);
4895 assert!(
4896 !collected
4897 .tools
4898 .iter()
4899 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4900 "hosted mechanism must not contribute the client-side tool_search tool"
4901 );
4902 assert!(
4903 collected.tool_definition_hooks.is_empty(),
4904 "hosted mechanism must not contribute a client-side deferral hook"
4905 );
4906 }
4907
4908 #[tokio::test]
4909 async fn test_collect_capabilities_no_tool_search_without_capability() {
4910 let registry = CapabilityRegistry::with_builtins();
4911
4912 let configs = vec![AgentCapabilityConfig {
4913 capability_ref: CapabilityId::new("current_time"),
4914 config: serde_json::json!({}),
4915 }];
4916
4917 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4918
4919 assert!(collected.tool_search.is_none());
4920 }
4921
4922 #[tokio::test]
4923 async fn test_collect_capabilities_tool_search_category_propagation() {
4924 let registry = CapabilityRegistry::with_builtins();
4925
4926 let configs = vec![
4928 AgentCapabilityConfig {
4929 capability_ref: CapabilityId::new("test_math"),
4930 config: serde_json::json!({}),
4931 },
4932 AgentCapabilityConfig {
4933 capability_ref: CapabilityId::new("openai_tool_search"),
4934 config: serde_json::json!({}),
4935 },
4936 ];
4937
4938 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4939
4940 assert!(collected.tool_search.is_some());
4942
4943 for tool_def in &collected.tool_definitions {
4945 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
4947 assert!(
4948 tool_def.category().is_some(),
4949 "Tool {} should have a category from its capability",
4950 tool_def.name()
4951 );
4952 }
4953 }
4954 }
4955
4956 #[tokio::test]
4957 async fn test_apply_capabilities_prompt_caching() {
4958 let registry = CapabilityRegistry::with_builtins();
4959 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4960
4961 let applied = apply_capabilities(
4962 base_runtime_agent.clone(),
4963 &["prompt_caching".to_string()],
4964 ®istry,
4965 &test_ctx(),
4966 )
4967 .await;
4968
4969 assert_eq!(
4970 applied.runtime_agent.system_prompt,
4971 base_runtime_agent.system_prompt
4972 );
4973 assert!(applied.tool_registry.is_empty());
4974 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
4975
4976 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
4977 assert!(prompt_cache.enabled);
4978 assert_eq!(
4979 prompt_cache.strategy,
4980 crate::driver_registry::PromptCacheStrategy::Auto
4981 );
4982 assert!(prompt_cache.gemini_cached_content.is_none());
4983 }
4984
4985 #[tokio::test]
4986 async fn test_apply_capabilities_openrouter_server_tools() {
4987 let registry = CapabilityRegistry::with_builtins();
4988 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4989
4990 let configs = vec![AgentCapabilityConfig {
4991 capability_ref: CapabilityId::new("openrouter_server_tools"),
4992 config: serde_json::json!({
4993 "tools": ["web_search", "datetime"],
4994 "web_search_max_results": 4,
4995 }),
4996 }];
4997
4998 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4999 let routing = collected
5000 .openrouter_routing
5001 .as_ref()
5002 .expect("server tools produce routing config");
5003 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
5004 assert_eq!(
5005 kinds,
5006 vec![
5007 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
5008 crate::driver_registry::OpenRouterServerToolKind::Datetime,
5009 ]
5010 );
5011
5012 let applied = apply_capabilities(
5015 base_runtime_agent,
5016 &["openrouter_server_tools".to_string()],
5017 ®istry,
5018 &test_ctx(),
5019 )
5020 .await;
5021 assert!(applied.tool_registry.is_empty());
5022 assert!(applied.runtime_agent.openrouter_routing.is_none());
5023 }
5024
5025 #[tokio::test]
5026 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
5027 let registry = CapabilityRegistry::with_builtins();
5028
5029 let configs = vec![AgentCapabilityConfig {
5030 capability_ref: CapabilityId::new("prompt_caching"),
5031 config: serde_json::json!({"strategy": "auto"}),
5032 }];
5033
5034 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5035
5036 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5037 assert!(prompt_cache.enabled);
5038 assert_eq!(
5039 prompt_cache.strategy,
5040 crate::driver_registry::PromptCacheStrategy::Auto
5041 );
5042 assert!(prompt_cache.gemini_cached_content.is_none());
5043 }
5044
5045 #[tokio::test]
5046 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
5047 let registry = CapabilityRegistry::with_builtins();
5048
5049 let configs = vec![AgentCapabilityConfig {
5050 capability_ref: CapabilityId::new("prompt_caching"),
5051 config: serde_json::json!({
5052 "strategy": "auto",
5053 "gemini_cached_content": "cachedContents/demo-cache"
5054 }),
5055 }];
5056
5057 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5058
5059 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5060 assert_eq!(
5061 prompt_cache.gemini_cached_content.as_deref(),
5062 Some("cachedContents/demo-cache")
5063 );
5064 }
5065
5066 struct SkillContributingCapability;
5071
5072 impl Capability for SkillContributingCapability {
5073 fn id(&self) -> &str {
5074 "contributes_skills"
5075 }
5076 fn name(&self) -> &str {
5077 "Contributes Skills"
5078 }
5079 fn description(&self) -> &str {
5080 "Test capability that contributes skills."
5081 }
5082 fn contribute_skills(&self) -> Vec<SkillContribution> {
5083 vec![
5084 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
5085 .with_files(vec![(
5086 "scripts/a.sh".to_string(),
5087 "#!/bin/sh\necho a\n".to_string(),
5088 )]),
5089 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
5090 .with_user_invocable(false),
5091 ]
5092 }
5093 }
5094
5095 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
5096 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
5097 MountSource::InlineFile { content, .. } => content.as_str(),
5098 _ => panic!("Expected InlineFile for SKILL.md"),
5099 }
5100 }
5101
5102 #[tokio::test]
5103 async fn test_contribute_skills_normalized_to_mounts() {
5104 let mut registry = CapabilityRegistry::new();
5105 registry.register(SkillContributingCapability);
5106
5107 let configs = vec![AgentCapabilityConfig {
5108 capability_ref: CapabilityId::new("contributes_skills"),
5109 config: serde_json::json!({}),
5110 }];
5111
5112 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5113
5114 let skill_mounts: Vec<_> = collected
5115 .mounts
5116 .iter()
5117 .filter(|m| m.path.starts_with("/.agents/skills/"))
5118 .collect();
5119 assert_eq!(skill_mounts.len(), 2);
5120
5121 for m in &skill_mounts {
5124 assert!(m.is_readonly());
5125 assert_eq!(m.capability_id, "contributes_skills");
5126 }
5127
5128 let alpha = skill_mounts
5129 .iter()
5130 .find(|m| m.path == "/.agents/skills/alpha-skill")
5131 .expect("alpha-skill mount missing");
5132 match &alpha.source {
5133 MountSource::InlineDirectory { entries } => {
5134 assert!(entries.contains_key("SKILL.md"));
5135 assert!(entries.contains_key("scripts/a.sh"));
5136 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5137 assert_eq!(parsed.name, "alpha-skill");
5138 assert!(parsed.user_invocable);
5139 }
5140 _ => panic!("Expected InlineDirectory"),
5141 }
5142
5143 let beta = skill_mounts
5144 .iter()
5145 .find(|m| m.path == "/.agents/skills/beta-skill")
5146 .expect("beta-skill mount missing");
5147 match &beta.source {
5148 MountSource::InlineDirectory { entries } => {
5149 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5150 assert!(!parsed.user_invocable);
5151 }
5152 _ => panic!("Expected InlineDirectory"),
5153 }
5154 }
5155
5156 #[tokio::test]
5157 async fn test_contribute_skills_default_empty() {
5158 let mut registry = CapabilityRegistry::new();
5161 registry.register(FilterTestCapability { priority: 0 });
5162
5163 let configs = vec![AgentCapabilityConfig {
5164 capability_ref: CapabilityId::new("filter_test"),
5165 config: serde_json::json!({}),
5166 }];
5167
5168 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5169 assert!(
5170 collected
5171 .mounts
5172 .iter()
5173 .all(|m| !m.path.starts_with("/.agents/skills/"))
5174 );
5175 }
5176
5177 struct LocalizedCapability;
5178
5179 impl Capability for LocalizedCapability {
5180 fn id(&self) -> &str {
5181 "localized"
5182 }
5183 fn name(&self) -> &str {
5184 "Localized"
5185 }
5186 fn description(&self) -> &str {
5187 "English description"
5188 }
5189 fn localizations(&self) -> Vec<CapabilityLocalization> {
5190 vec![
5191 CapabilityLocalization {
5192 locale: "en",
5193 name: None,
5194 description: None,
5195 config_description: Some("Controls things."),
5196 config_overlay: None,
5197 },
5198 CapabilityLocalization {
5199 locale: "uk",
5200 name: Some("Локалізована"),
5201 description: Some("Український опис"),
5202 config_description: Some("Керує налаштуваннями."),
5203 config_overlay: None,
5204 },
5205 ]
5206 }
5207 }
5208
5209 #[test]
5210 fn localized_name_falls_back_exact_language_then_base() {
5211 let cap = LocalizedCapability;
5212 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
5214 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
5215 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
5217 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
5219 assert_eq!(cap.localized_name(None), "Localized");
5220 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
5221 assert_eq!(cap.localized_description(Some("de")), "English description");
5222 }
5223
5224 #[test]
5225 fn describe_schema_resolves_config_description_per_locale() {
5226 let cap = LocalizedCapability;
5227 assert_eq!(
5228 cap.describe_schema(Some("uk-UA")).as_deref(),
5229 Some("Керує налаштуваннями.")
5230 );
5231 assert_eq!(
5233 cap.describe_schema(Some("pl")).as_deref(),
5234 Some("Controls things.")
5235 );
5236 assert_eq!(
5237 cap.describe_schema(None).as_deref(),
5238 Some("Controls things.")
5239 );
5240 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
5242 }
5243}