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_output_distillation;
146mod tool_output_persistence;
147mod tool_search;
148pub mod user_hooks;
149mod web_fetch;
150
151pub use a2a_delegation::{
153 A2A_AGENT_DELEGATION_CAPABILITY_ID, A2aAgentDelegationCapability, SpawnAgentTool,
154};
155#[cfg(feature = "ui-capabilities")]
156pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
157pub use agent_handoff::{
158 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
159 MessageAgentHandoffTool, StartAgentHandoffTool,
160};
161pub use agent_instructions::{
162 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
163 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
164 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
165};
166pub use attach_skill::{
167 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
168 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
169 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
170};
171pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
172pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
173pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
174pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
175pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
176pub use compaction::{
177 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
178 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
179 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
180 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
181 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
182 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
183 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
184};
185pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
186pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
187pub use declarative::{
188 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
189 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
190 declarative_capability_info, hydrate_declarative_capability_config,
191 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
192 plugin_capability_info, validate_declarative_capability_definition,
193};
194pub use error_disclosure::{
195 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
196};
197pub use fake_aws::{
198 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
199 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
200 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
201 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
202};
203pub use fake_crm::{
204 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
205 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
206 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
207};
208pub use fake_financial::{
209 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
210 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
211 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
212 FinanceListTransactionsTool,
213};
214pub use fake_warehouse::{
215 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
216 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
217 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
218 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
219};
220pub use file_system::{
221 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
222 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
223};
224pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
225pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
226pub use infinity_context::{
227 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
228};
229pub use knowledge_base::{
230 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
231 validate_knowledge_base_config,
232};
233pub use knowledge_index::{
234 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
235 validate_knowledge_index_config,
236};
237pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
238pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
239pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
240pub use mcp::{
241 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
242 parse_mcp_capability_id,
243};
244pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
245pub use message_metadata::{
246 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
247 MessageMetadataField, render_annotation,
248};
249pub use model_scout::{
250 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
251 RouterUpdateProposal, compute_score, rank_results,
252};
253pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
254pub use openai_tool_search::{
255 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
256 model_supports_native_tool_search,
257};
258pub use openrouter_server_tools::{
259 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
260};
261pub use openrouter_workspace::{
262 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
263 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
264 detect_policy_drift,
265};
266#[cfg(feature = "ui-capabilities")]
267pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
268pub use platform_management::{
269 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
270 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
271 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
272};
273pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
274pub use prompt_canary_guardrail::{
275 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
276 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
277 REASON_CODE_SYSTEM_PROMPT_LEAK,
278};
279pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
280pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
281pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
282pub use session::{
283 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
284};
285pub use session_sandbox::{
286 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
287 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
288};
289pub use session_schedule::{
290 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
291 SessionScheduleCapability,
292};
293pub use session_sql_database::{
294 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
295 SqlSchemaTool,
296};
297pub use session_storage::{
298 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
299 is_internal_session_kv_key,
300};
301pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
302pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
303pub use skills_scoped::{
304 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
305};
306pub use stateless_todo_list::{
307 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
308};
309pub use subagents::{SUBAGENTS_CAPABILITY_ID, SubagentCapability};
310pub use bashkit_shell::{
312 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
313};
314pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
315pub use test_math::{
316 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
317};
318pub use test_weather::{
319 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
320};
321pub use tool_output_distillation::{
322 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
323};
324pub use tool_output_persistence::{
325 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
326};
327pub use tool_search::{
328 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
329};
330pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
331pub use web_fetch::{
332 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
333 derive_bot_auth_public_key,
334};
335
336pub struct SystemPromptContext {
346 pub session_id: SessionId,
348 pub locale: Option<String>,
350 pub file_store: Option<Arc<dyn SessionFileSystem>>,
352 pub model: Option<String>,
358}
359
360impl SystemPromptContext {
361 pub fn without_file_store(session_id: SessionId) -> Self {
363 Self {
364 session_id,
365 locale: None,
366 file_store: None,
367 model: None,
368 }
369 }
370
371 pub fn with_model(mut self, model: impl Into<String>) -> Self {
373 self.model = Some(model.into());
374 self
375 }
376}
377
378#[derive(Debug, Clone)]
430pub struct CapabilityLocalization {
431 pub locale: &'static str,
433 pub name: Option<&'static str>,
435 pub description: Option<&'static str>,
437 pub config_description: Option<&'static str>,
442 pub config_overlay: Option<serde_json::Value>,
448}
449
450impl CapabilityLocalization {
451 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
453 Self {
454 locale,
455 name: Some(name),
456 description: Some(description),
457 config_description: None,
458 config_overlay: None,
459 }
460 }
461}
462
463pub fn resolve_localized_field<T>(
467 localizations: &[CapabilityLocalization],
468 locale: Option<&str>,
469 field: impl Fn(&CapabilityLocalization) -> Option<T>,
470) -> Option<T> {
471 let mut candidates: Vec<String> = Vec::new();
472 if let Some(raw) = locale {
473 let normalized = raw.trim().replace('_', "-").to_lowercase();
474 if !normalized.is_empty() {
475 if let Some((language, _)) = normalized.split_once('-') {
476 let language = language.to_string();
477 candidates.push(normalized);
478 candidates.push(language);
479 } else {
480 candidates.push(normalized);
481 }
482 }
483 }
484 candidates.push("en".to_string());
485
486 for candidate in candidates {
487 let hit = localizations
488 .iter()
489 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
490 .and_then(&field);
491 if hit.is_some() {
492 return hit;
493 }
494 }
495 None
496}
497
498#[async_trait]
499pub trait Capability: Send + Sync {
500 fn id(&self) -> &str;
502
503 fn aliases(&self) -> Vec<&'static str> {
512 vec![]
513 }
514
515 fn name(&self) -> &str;
517
518 fn description(&self) -> &str;
520
521 fn localizations(&self) -> Vec<CapabilityLocalization> {
526 vec![]
527 }
528
529 fn localized_name(&self, locale: Option<&str>) -> String {
532 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
533 .unwrap_or_else(|| self.name())
534 .to_string()
535 }
536
537 fn localized_description(&self, locale: Option<&str>) -> String {
539 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
540 .unwrap_or_else(|| self.description())
541 .to_string()
542 }
543
544 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
548 resolve_localized_field(&self.localizations(), locale, |entry| {
549 entry.config_description
550 })
551 .map(str::to_string)
552 }
553
554 fn status(&self) -> CapabilityStatus {
556 CapabilityStatus::Available
557 }
558
559 fn icon(&self) -> Option<&str> {
561 None
562 }
563
564 fn category(&self) -> Option<&str> {
566 None
567 }
568
569 fn is_guardrail(&self) -> bool {
574 false
575 }
576
577 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
588 None
589 }
590
591 fn system_prompt_addition(&self) -> Option<&str> {
611 None
612 }
613
614 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
626 self.system_prompt_addition().map(|addition| {
627 format!(
628 "<capability id=\"{}\">\n{}\n</capability>",
629 self.id(),
630 addition
631 )
632 })
633 }
634
635 fn system_prompt_preview(&self) -> Option<String> {
641 self.system_prompt_addition().map(|s| s.to_string())
642 }
643
644 fn tools(&self) -> Vec<Box<dyn Tool>> {
646 vec![]
647 }
648
649 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
657 self.tools()
658 }
659
660 async fn system_prompt_contribution_with_config(
667 &self,
668 ctx: &SystemPromptContext,
669 _config: &serde_json::Value,
670 ) -> Option<String> {
671 self.system_prompt_contribution(ctx).await
672 }
673
674 fn tool_definitions(&self) -> Vec<ToolDefinition> {
677 self.tools().iter().map(|t| t.to_definition()).collect()
678 }
679
680 fn mounts(&self) -> Vec<MountPoint> {
688 vec![]
689 }
690
691 fn dependencies(&self) -> Vec<&'static str> {
700 vec![]
701 }
702
703 fn features(&self) -> Vec<&'static str> {
718 vec![]
719 }
720
721 fn config_schema(&self) -> Option<serde_json::Value> {
727 None
728 }
729
730 fn config_ui_schema(&self) -> Option<serde_json::Value> {
735 None
736 }
737
738 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
744 Ok(())
745 }
746
747 fn mcp_servers(&self) -> ScopedMcpServers {
753 ScopedMcpServers::default()
754 }
755
756 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
758 self.mcp_servers()
759 }
760
761 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
774 None
775 }
776
777 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
785 None
786 }
787
788 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
799 vec![]
800 }
801
802 fn pre_tool_use_hooks_with_config(
807 &self,
808 _config: &serde_json::Value,
809 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
810 self.pre_tool_use_hooks()
811 }
812
813 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
821 vec![]
822 }
823
824 fn post_tool_exec_hooks_with_config(
829 &self,
830 _config: &serde_json::Value,
831 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
832 self.post_tool_exec_hooks()
833 }
834
835 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
844 vec![]
845 }
846
847 fn tool_definition_hooks_with_config(
852 &self,
853 _config: &serde_json::Value,
854 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
855 self.tool_definition_hooks()
856 }
857
858 fn tool_definition_hooks_with_context(
868 &self,
869 _ctx: &SystemPromptContext,
870 config: &serde_json::Value,
871 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
872 self.tool_definition_hooks_with_config(config)
873 }
874
875 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
883 vec![]
884 }
885
886 fn narrate(
900 &self,
901 _tool_def: Option<&ToolDefinition>,
902 tool_call: &ToolCall,
903 phase: crate::tool_narration::ToolNarrationPhase,
904 locale: Option<&str>,
905 ) -> Option<String> {
906 self.tools()
907 .iter()
908 .find(|tool| tool.name() == tool_call.name)
909 .and_then(|tool| tool.narrate(tool_call, phase, locale))
910 }
911
912 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
928 vec![]
929 }
930
931 fn user_hooks_with_config(
937 &self,
938 _config: &serde_json::Value,
939 ) -> Vec<crate::user_hook_types::UserHookSpec> {
940 self.user_hooks()
941 }
942
943 fn risk_level(&self) -> RiskLevel {
951 RiskLevel::Low
952 }
953
954 fn commands(&self) -> Vec<CommandDescriptor> {
962 vec![]
963 }
964
965 async fn execute_command(
979 &self,
980 request: &ExecuteCommandRequest,
981 _ctx: &CommandExecutionContext,
982 ) -> crate::error::Result<CommandResult> {
983 Err(crate::error::AgentLoopError::config(format!(
984 "capability {} declared command /{} but does not implement execute_command",
985 self.id(),
986 request.name,
987 )))
988 }
989
990 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
998 vec![]
999 }
1000
1001 fn contribute_skills(&self) -> Vec<SkillContribution> {
1011 vec![]
1012 }
1013
1014 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1025 vec![]
1026 }
1027}
1028
1029pub trait ToolDefinitionHook: Send + Sync {
1030 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1031
1032 fn applies_with_native_tool_search(&self) -> bool {
1037 true
1038 }
1039}
1040
1041pub trait ToolCallHook: Send + Sync {
1042 fn narration(
1043 &self,
1044 _tool_def: Option<&ToolDefinition>,
1045 _tool_call: &ToolCall,
1046 _phase: crate::tool_narration::ToolNarrationPhase,
1047 _locale: Option<&str>,
1048 ) -> Option<String> {
1049 None
1050 }
1051
1052 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1053 tool_call
1054 }
1055}
1056
1057pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1063
1064impl ToolCallHook for CapabilityNarrationHook {
1065 fn narration(
1066 &self,
1067 tool_def: Option<&ToolDefinition>,
1068 tool_call: &ToolCall,
1069 phase: crate::tool_narration::ToolNarrationPhase,
1070 locale: Option<&str>,
1071 ) -> Option<String> {
1072 self.0.narrate(tool_def, tool_call, phase, locale)
1073 }
1074}
1075
1076#[derive(
1080 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1081)]
1082#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1083#[cfg_attr(feature = "openapi", schema(example = "low"))]
1084#[serde(rename_all = "lowercase")]
1085pub enum RiskLevel {
1086 Low,
1088 Medium,
1090 High,
1092}
1093
1094#[derive(Debug, Clone, Serialize, Deserialize)]
1100#[serde(rename_all = "snake_case")]
1101pub enum BlueprintModel {
1102 Fixed(String),
1104 Default(String),
1106 Inherit,
1108}
1109
1110pub struct AgentBlueprint {
1116 pub id: &'static str,
1118 pub name: &'static str,
1120 pub description: &'static str,
1122 pub model: BlueprintModel,
1124 pub system_prompt: &'static str,
1126 pub tools: Vec<Box<dyn Tool>>,
1128 pub max_turns: Option<usize>,
1130 pub config_schema: Option<serde_json::Value>,
1132}
1133
1134impl AgentBlueprint {
1135 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1137 self.tools.iter().map(|t| t.to_definition()).collect()
1138 }
1139}
1140
1141impl std::fmt::Debug for AgentBlueprint {
1142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1143 f.debug_struct("AgentBlueprint")
1144 .field("id", &self.id)
1145 .field("name", &self.name)
1146 .field("model", &self.model)
1147 .field("tool_count", &self.tools.len())
1148 .field("max_turns", &self.max_turns)
1149 .finish()
1150 }
1151}
1152
1153#[derive(Clone)]
1180pub struct CapabilityRegistry {
1181 capabilities: HashMap<String, Arc<dyn Capability>>,
1182 aliases: HashMap<String, String>,
1184}
1185
1186impl CapabilityRegistry {
1187 pub fn new() -> Self {
1189 Self {
1190 capabilities: HashMap::new(),
1191 aliases: HashMap::new(),
1192 }
1193 }
1194
1195 pub fn with_builtins() -> Self {
1200 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1201 }
1202
1203 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1208 let mut registry = Self::new();
1209
1210 registry.register(AgentInstructionsCapability);
1212 registry.register(HumanIntentCapability);
1213 registry.register(NoopCapability);
1214 registry.register(CurrentTimeCapability);
1215 registry.register(MessageMetadataCapability);
1216 registry.register(ResearchCapability);
1217 registry.register(ModelScoutCapability);
1218 registry.register(OpenRouterWorkspaceCapability);
1219 registry.register(OpenRouterServerToolsCapability);
1220 registry.register(PlatformManagementCapability);
1221 registry.register(FileSystemCapability);
1222 registry.register(MemoryCapability);
1223 registry.register(SessionStorageCapability);
1224 registry.register(SessionCapability);
1225 registry.register(SessionSqlDatabaseCapability);
1226 registry.register(TestMathCapability);
1227 registry.register(TestWeatherCapability);
1228 registry.register(StatelessTodoListCapability);
1229 registry.register(WebFetchCapability::from_env());
1230 registry.register(BashkitShellCapability);
1231 registry.register(BackgroundExecutionCapability);
1232 registry.register(SessionScheduleCapability);
1233 registry.register(BtwCapability);
1234 registry.register(InfinityContextCapability);
1235 registry.register(budgeting::BudgetingCapability);
1236 registry.register(SelfBudgetCapability);
1237 registry.register(CompactionCapability);
1238 registry.register(ErrorDisclosureCapability);
1239
1240 registry.register(OpenAiToolSearchCapability::new());
1242 registry.register(ClaudeToolSearchCapability::new());
1244 registry.register(ToolSearchCapability::new());
1246 registry.register(AutoToolSearchCapability::new());
1248 registry.register(PromptCachingCapability::new());
1249
1250 registry.register(SkillsCapability);
1252
1253 registry.register(SubagentCapability);
1255
1256 registry.register(SessionTasksCapability);
1258
1259 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1263 registry.register(AgentHandoffCapability);
1264 registry.register(A2aAgentDelegationCapability);
1265 }
1266
1267 registry.register(SystemCommandsCapability);
1269
1270 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1272 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1273
1274 registry.register(user_hooks::UserHooksCapability);
1277
1278 registry.register(LoopDetectionCapability);
1280
1281 registry.register(PromptCanaryGuardrailCapability);
1284
1285 registry.register(GuardrailsCapability);
1288
1289 #[cfg(feature = "ui-capabilities")]
1291 {
1292 registry.register(OpenUiCapability);
1293 registry.register(A2UiCapability);
1294 }
1295
1296 registry.register(SampleDataCapability);
1298
1299 registry.register(DataKnowledgeCapability);
1301
1302 registry.register(KnowledgeBaseCapability);
1304
1305 registry.register(KnowledgeIndexCapability);
1307
1308 registry.register(FakeWarehouseCapability);
1310 registry.register(FakeAwsCapability);
1311 registry.register(FakeCrmCapability);
1312 registry.register(FakeFinancialCapability);
1313
1314 let internal_flags = crate::InternalFeatureFlags::from_env();
1316 if internal_flags.session_sandbox {
1317 registry.register(SessionSandboxCapability);
1318 }
1319
1320 if internal_flags.lua {
1324 registry.register(LuaCapability);
1325 registry.register(LuaCodeModeCapability);
1328 }
1329 for plugin in inventory::iter::<IntegrationPlugin>() {
1330 if (!plugin.experimental_only || grade.experimental_features_enabled())
1331 && plugin
1332 .feature_flag
1333 .is_none_or(|f| internal_flags.is_enabled(f))
1334 {
1335 registry.register_boxed((plugin.factory)());
1336 }
1337 }
1338
1339 registry
1340 }
1341
1342 pub fn register(&mut self, capability: impl Capability + 'static) {
1344 self.register_arc(Arc::new(capability));
1345 }
1346
1347 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1349 self.register_arc(Arc::from(capability));
1350 }
1351
1352 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1354 let canonical = capability.id().to_string();
1355 for alias in capability.aliases() {
1356 self.aliases.insert(alias.to_string(), canonical.clone());
1357 }
1358 self.capabilities.insert(canonical, capability);
1359 }
1360
1361 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1363 self.capabilities
1364 .get(id)
1365 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1366 }
1367
1368 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1373 if self.capabilities.contains_key(id) {
1374 Some(id)
1375 } else {
1376 self.aliases
1377 .get(id)
1378 .filter(|c| self.capabilities.contains_key(*c))
1379 .map(String::as_str)
1380 }
1381 }
1382
1383 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1385 let canonical = self.canonical_id(id)?.to_string();
1386 let removed = self.capabilities.remove(&canonical);
1387 self.aliases.retain(|_, target| *target != canonical);
1388 removed
1389 }
1390
1391 pub fn has(&self, id: &str) -> bool {
1393 self.get(id).is_some()
1394 }
1395
1396 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1398 self.capabilities.values().collect()
1399 }
1400
1401 pub fn len(&self) -> usize {
1403 self.capabilities.len()
1404 }
1405
1406 pub fn is_empty(&self) -> bool {
1408 self.capabilities.is_empty()
1409 }
1410
1411 pub fn builder() -> CapabilityRegistryBuilder {
1413 CapabilityRegistryBuilder::new()
1414 }
1415
1416 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1420 for cap in self.capabilities.values() {
1421 for bp in cap.agent_blueprints() {
1422 if bp.id == id {
1423 return Some(bp);
1424 }
1425 }
1426 }
1427 None
1428 }
1429
1430 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1434 for (capability_id, cap) in &self.capabilities {
1435 for bp in cap.agent_blueprints() {
1436 if bp.id == id {
1437 return Some((capability_id.clone(), bp));
1438 }
1439 }
1440 }
1441 None
1442 }
1443
1444 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1446 self.capabilities
1447 .values()
1448 .flat_map(|cap| cap.agent_blueprints())
1449 .collect()
1450 }
1451}
1452
1453impl Default for CapabilityRegistry {
1454 fn default() -> Self {
1455 Self::with_builtins()
1456 }
1457}
1458
1459impl std::fmt::Debug for CapabilityRegistry {
1460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1461 let ids: Vec<_> = self.capabilities.keys().collect();
1462 f.debug_struct("CapabilityRegistry")
1463 .field("capabilities", &ids)
1464 .finish()
1465 }
1466}
1467
1468pub struct CapabilityRegistryBuilder {
1470 registry: CapabilityRegistry,
1471}
1472
1473impl CapabilityRegistryBuilder {
1474 pub fn new() -> Self {
1476 Self {
1477 registry: CapabilityRegistry::new(),
1478 }
1479 }
1480
1481 pub fn with_builtins() -> Self {
1483 Self {
1484 registry: CapabilityRegistry::with_builtins(),
1485 }
1486 }
1487
1488 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1490 self.registry.register(capability);
1491 self
1492 }
1493
1494 pub fn build(self) -> CapabilityRegistry {
1496 self.registry
1497 }
1498}
1499
1500impl Default for CapabilityRegistryBuilder {
1501 fn default() -> Self {
1502 Self::new()
1503 }
1504}
1505
1506pub struct ModelViewContext<'a> {
1512 pub session_id: SessionId,
1513 pub prior_usage: Option<&'a TokenUsage>,
1514}
1515
1516pub trait ModelViewProvider: Send + Sync {
1522 fn apply_model_view(
1523 &self,
1524 messages: Vec<Message>,
1525 config: &serde_json::Value,
1526 context: &ModelViewContext<'_>,
1527 ) -> Vec<Message>;
1528
1529 fn priority(&self) -> i32 {
1530 0
1531 }
1532}
1533
1534pub struct CollectedCapabilities {
1539 pub system_prompt_parts: Vec<String>,
1541 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1543 pub tools: Vec<Box<dyn Tool>>,
1545 pub tool_definitions: Vec<ToolDefinition>,
1547 pub mounts: Vec<MountPoint>,
1549 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1551 pub applied_ids: Vec<String>,
1553 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1555 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1557 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1560 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1562 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1564 pub mcp_servers: ScopedMcpServers,
1566 }
1572
1573#[derive(Debug, Clone, PartialEq, Eq)]
1574pub struct SystemPromptAttribution {
1575 pub capability_id: String,
1576 pub content: String,
1577}
1578
1579impl CollectedCapabilities {
1580 pub fn system_prompt_prefix(&self) -> Option<String> {
1583 if self.system_prompt_parts.is_empty() {
1584 None
1585 } else {
1586 Some(self.system_prompt_parts.join("\n\n"))
1587 }
1588 }
1589
1590 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1594 for (provider, config) in &self.message_filter_providers {
1596 provider.apply_filters(query, config);
1597 }
1598 }
1599
1600 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1603 for (provider, config) in &self.message_filter_providers {
1604 provider.post_load(messages, config);
1605 }
1606 }
1607
1608 pub fn has_message_filters(&self) -> bool {
1610 !self.message_filter_providers.is_empty()
1611 }
1612}
1613
1614pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1619 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1620 return base_system_prompt.to_string();
1621 };
1622
1623 if base_system_prompt.is_empty() {
1624 return additions.to_string();
1625 }
1626
1627 if base_system_prompt.contains("<system-prompt>") {
1628 format!("{base_system_prompt}\n\n{additions}")
1629 } else {
1630 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1631 }
1632}
1633
1634pub struct CollectedMessageFilters {
1641 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1643}
1644
1645pub struct CollectedModelViewProviders {
1647 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1649}
1650
1651impl CollectedMessageFilters {
1657 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1659 for (provider, config) in &self.message_filter_providers {
1660 provider.apply_filters(query, config);
1661 }
1662 }
1663
1664 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1666 for (provider, config) in &self.message_filter_providers {
1667 provider.post_load(messages, config);
1668 }
1669 }
1670}
1671
1672impl CollectedModelViewProviders {
1673 pub fn apply_model_view(
1675 &self,
1676 mut messages: Vec<Message>,
1677 context: &ModelViewContext<'_>,
1678 ) -> Vec<Message> {
1679 for (provider, config) in &self.model_view_providers {
1680 messages = provider.apply_model_view(messages, config, context);
1681 }
1682 messages
1683 }
1684}
1685
1686fn compaction_is_enabled(
1692 capability_configs: &[AgentCapabilityConfig],
1693 registry: &CapabilityRegistry,
1694) -> bool {
1695 capability_configs.iter().any(|cap_config| {
1696 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
1697 && registry
1698 .get(cap_config.capability_ref.as_str())
1699 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
1700 })
1701}
1702
1703fn message_filter_config_for(
1712 cap_id: &str,
1713 base: &serde_json::Value,
1714 compaction_on: bool,
1715) -> serde_json::Value {
1716 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
1717 return base.clone();
1718 }
1719 let mut config = base.clone();
1720 match config.as_object_mut() {
1721 Some(map) => {
1722 map.insert(
1723 "compaction_active".to_string(),
1724 serde_json::Value::Bool(true),
1725 );
1726 }
1727 None => {
1728 config = serde_json::json!({ "compaction_active": true });
1729 }
1730 }
1731 config
1732}
1733
1734pub fn collect_message_filters_only(
1740 capability_configs: &[AgentCapabilityConfig],
1741 registry: &CapabilityRegistry,
1742) -> CollectedMessageFilters {
1743 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1744 Vec::new();
1745 let compaction_on = compaction_is_enabled(capability_configs, registry);
1746
1747 for cap_config in capability_configs {
1748 let cap_id = cap_config.capability_ref.as_str();
1749 if let Some(capability) = registry.get(cap_id) {
1750 if capability.status() != CapabilityStatus::Available {
1751 continue;
1752 }
1753 let effective: &dyn Capability = capability
1756 .resolve_for_model(None)
1757 .unwrap_or_else(|| capability.as_ref());
1758 if let Some(provider) = effective.message_filter_provider() {
1759 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
1760 message_filter_providers.push((provider, config));
1761 }
1762 }
1763 }
1764
1765 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1766
1767 CollectedMessageFilters {
1768 message_filter_providers,
1769 }
1770}
1771
1772pub fn collect_model_view_providers(
1779 capability_configs: &[AgentCapabilityConfig],
1780 registry: &CapabilityRegistry,
1781 model: Option<&str>,
1782) -> CollectedModelViewProviders {
1783 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1784
1785 for cap_config in capability_configs {
1786 let cap_id = cap_config.capability_ref.as_str();
1787 if let Some(capability) = registry.get(cap_id) {
1788 if capability.status() != CapabilityStatus::Available {
1789 continue;
1790 }
1791 let effective: &dyn Capability = capability
1792 .resolve_for_model(model)
1793 .unwrap_or_else(|| capability.as_ref());
1794 if let Some(provider) = effective.model_view_provider() {
1795 model_view_providers.push((provider, cap_config.config.clone()));
1796 }
1797 }
1798 }
1799
1800 model_view_providers.sort_by_key(|(p, _)| p.priority());
1801
1802 CollectedModelViewProviders {
1803 model_view_providers,
1804 }
1805}
1806
1807pub fn collect_capability_mcp_servers(
1808 capability_configs: &[AgentCapabilityConfig],
1809 registry: &CapabilityRegistry,
1810) -> ScopedMcpServers {
1811 let mut servers = ScopedMcpServers::default();
1812
1813 for cap_config in capability_configs {
1814 let cap_id = cap_config.capability_ref.as_str();
1815 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
1818 if let Ok(definition) =
1819 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1820 {
1821 if definition.status != CapabilityStatus::Available {
1822 continue;
1823 }
1824 if let Some(contributed) = definition.mcp_servers {
1825 servers = merge_scoped_mcp_servers(&servers, &contributed);
1826 }
1827 }
1828 continue;
1829 }
1830 if let Some(capability) = registry.get(cap_id) {
1831 if capability.status() != CapabilityStatus::Available {
1832 continue;
1833 }
1834 servers = merge_scoped_mcp_servers(
1835 &servers,
1836 &capability.mcp_servers_with_config(&cap_config.config),
1837 );
1838 }
1839 }
1840
1841 servers
1842}
1843
1844pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1851
1852#[derive(Debug, Clone, PartialEq, Eq)]
1854pub enum DependencyError {
1855 CircularDependency {
1857 capability_id: String,
1859 chain: Vec<String>,
1861 },
1862 TooManyCapabilities {
1864 count: usize,
1866 max: usize,
1868 },
1869}
1870
1871impl std::fmt::Display for DependencyError {
1872 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1873 match self {
1874 DependencyError::CircularDependency {
1875 capability_id,
1876 chain,
1877 } => {
1878 write!(
1879 f,
1880 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1881 capability_id,
1882 chain.join(" -> "),
1883 capability_id
1884 )
1885 }
1886 DependencyError::TooManyCapabilities { count, max } => {
1887 write!(
1888 f,
1889 "Too many capabilities after resolution: {} (max: {})",
1890 count, max
1891 )
1892 }
1893 }
1894 }
1895}
1896
1897impl std::error::Error for DependencyError {}
1898
1899#[derive(Debug, Clone)]
1901pub struct ResolvedCapabilities {
1902 pub resolved_ids: Vec<String>,
1905 pub added_as_dependencies: Vec<String>,
1907 pub user_selected: Vec<String>,
1909}
1910
1911pub fn resolve_dependencies(
1931 selected_ids: &[String],
1932 registry: &CapabilityRegistry,
1933) -> Result<ResolvedCapabilities, DependencyError> {
1934 use std::collections::HashSet;
1935
1936 let user_selected: HashSet<String> = selected_ids
1938 .iter()
1939 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
1940 .collect();
1941 let mut resolved: Vec<String> = Vec::new();
1942 let mut resolved_set: HashSet<String> = HashSet::new();
1943 let mut added_as_dependencies: Vec<String> = Vec::new();
1944
1945 for cap_id in selected_ids {
1947 resolve_single_capability(
1948 cap_id,
1949 registry,
1950 &mut resolved,
1951 &mut resolved_set,
1952 &mut added_as_dependencies,
1953 &user_selected,
1954 &mut Vec::new(), )?;
1956 }
1957
1958 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
1960 return Err(DependencyError::TooManyCapabilities {
1961 count: resolved.len(),
1962 max: MAX_RESOLVED_CAPABILITIES,
1963 });
1964 }
1965
1966 Ok(ResolvedCapabilities {
1967 resolved_ids: resolved,
1968 added_as_dependencies,
1969 user_selected: selected_ids.to_vec(),
1970 })
1971}
1972
1973pub fn resolve_capability_configs(
1978 selected_configs: &[AgentCapabilityConfig],
1979 registry: &CapabilityRegistry,
1980) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
1981 let mut selected_ids: Vec<String> = Vec::new();
1982 for config in selected_configs {
1983 if (is_declarative_capability(config.capability_id())
1986 || is_plugin_capability(config.capability_id()))
1987 && let Ok(definition) =
1988 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
1989 {
1990 selected_ids.extend(definition.dependencies);
1991 }
1992 selected_ids.push(config.capability_id().to_string());
1993 }
1994 let resolved = resolve_dependencies(&selected_ids, registry)?;
1995
1996 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
1999 .iter()
2000 .map(|config| {
2001 let id = config.capability_id();
2002 let id = registry.canonical_id(id).unwrap_or(id);
2003 (id.to_string(), config.config.clone())
2004 })
2005 .collect();
2006
2007 Ok(resolved
2008 .resolved_ids
2009 .into_iter()
2010 .map(|capability_id| {
2011 explicit_configs
2012 .get(&capability_id)
2013 .cloned()
2014 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2015 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2016 })
2017 .collect())
2018}
2019
2020fn resolve_single_capability(
2022 cap_id: &str,
2023 registry: &CapabilityRegistry,
2024 resolved: &mut Vec<String>,
2025 resolved_set: &mut std::collections::HashSet<String>,
2026 added_as_dependencies: &mut Vec<String>,
2027 user_selected: &std::collections::HashSet<String>,
2028 visiting: &mut Vec<String>,
2029) -> Result<(), DependencyError> {
2030 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2034
2035 if resolved_set.contains(cap_id) {
2037 return Ok(());
2038 }
2039
2040 if visiting.contains(&cap_id.to_string()) {
2042 return Err(DependencyError::CircularDependency {
2043 capability_id: cap_id.to_string(),
2044 chain: visiting.clone(),
2045 });
2046 }
2047
2048 let capability = match registry.get(cap_id) {
2050 Some(cap) => cap,
2051 None => {
2052 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2056 && !resolved_set.contains(cap_id)
2057 {
2058 resolved.push(cap_id.to_string());
2059 resolved_set.insert(cap_id.to_string());
2060 if !user_selected.contains(cap_id) {
2061 added_as_dependencies.push(cap_id.to_string());
2062 }
2063 }
2064 return Ok(());
2065 }
2066 };
2067
2068 visiting.push(cap_id.to_string());
2070
2071 for dep_id in capability.dependencies() {
2073 resolve_single_capability(
2074 dep_id,
2075 registry,
2076 resolved,
2077 resolved_set,
2078 added_as_dependencies,
2079 user_selected,
2080 visiting,
2081 )?;
2082 }
2083
2084 visiting.pop();
2086
2087 if !resolved_set.contains(cap_id) {
2089 resolved.push(cap_id.to_string());
2090 resolved_set.insert(cap_id.to_string());
2091
2092 if !user_selected.contains(cap_id) {
2094 added_as_dependencies.push(cap_id.to_string());
2095 }
2096 }
2097
2098 Ok(())
2099}
2100
2101pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2106 use std::collections::HashSet;
2107
2108 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2109 Ok(resolved) => resolved.resolved_ids,
2110 Err(_) => capability_ids.to_vec(),
2111 };
2112
2113 let mut seen = HashSet::new();
2114 let mut features = Vec::new();
2115 for cap_id in &resolved_ids {
2116 if let Some(cap) = registry.get(cap_id) {
2117 for feature in cap.features() {
2118 if seen.insert(feature) {
2119 features.push(feature.to_string());
2120 }
2121 }
2122 }
2123 }
2124 features
2125}
2126
2127pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2130 registry
2131 .get(cap_id)
2132 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2133 .unwrap_or_default()
2134}
2135
2136pub async fn collect_capabilities(
2152 capability_ids: &[String],
2153 registry: &CapabilityRegistry,
2154 ctx: &SystemPromptContext,
2155) -> CollectedCapabilities {
2156 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2159 Ok(resolved) => resolved.resolved_ids,
2160 Err(e) => {
2161 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2162 capability_ids.to_vec()
2163 }
2164 };
2165
2166 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2168 .iter()
2169 .map(|id| AgentCapabilityConfig {
2170 capability_ref: CapabilityId::new(id),
2171 config: serde_json::Value::Object(serde_json::Map::new()),
2172 })
2173 .collect();
2174
2175 collect_capabilities_with_configs(&configs, registry, ctx).await
2176}
2177
2178pub async fn collect_capabilities_with_configs(
2189 capability_configs: &[AgentCapabilityConfig],
2190 registry: &CapabilityRegistry,
2191 ctx: &SystemPromptContext,
2192) -> CollectedCapabilities {
2193 let mut system_prompt_parts: Vec<String> = Vec::new();
2194 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2195 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2196 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2197 let mut mounts: Vec<MountPoint> = Vec::new();
2198 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2199 Vec::new();
2200 let mut applied_ids: Vec<String> = Vec::new();
2201 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2202 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2203 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2204 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2205 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2206 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2209 let mut mcp_servers = ScopedMcpServers::default();
2210 let compaction_on = compaction_is_enabled(capability_configs, registry);
2211
2212 for cap_config in capability_configs {
2213 let cap_id = cap_config.capability_ref.as_str();
2214 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2219 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2220 cap_config.config.clone(),
2221 ) {
2222 Ok(definition) => {
2223 if definition.status != CapabilityStatus::Available {
2224 continue;
2225 }
2226
2227 if let Some(prompt) = definition.system_prompt.as_deref() {
2228 let contribution =
2229 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2230 system_prompt_attributions.push(SystemPromptAttribution {
2231 capability_id: cap_id.to_string(),
2232 content: contribution.clone(),
2233 });
2234 system_prompt_parts.push(contribution);
2235 }
2236
2237 mounts.extend(definition.mounts(cap_id));
2238 if let Some(ref servers) = definition.mcp_servers {
2239 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2240 }
2241 for skill in definition.skill_contributions() {
2242 mounts.push(skill.to_mount(cap_id));
2243 }
2244
2245 applied_ids.push(cap_id.to_string());
2246 }
2247 Err(error) => {
2248 tracing::warn!(
2249 capability_id = %cap_id,
2250 error = %error,
2251 "Skipping invalid declarative/plugin capability config"
2252 );
2253 }
2254 }
2255 continue;
2256 }
2257 if let Some(capability) = registry.get(cap_id) {
2258 if capability.status() != CapabilityStatus::Available {
2260 continue;
2261 }
2262
2263 let effective: &dyn Capability =
2275 match capability.resolve_for_model(ctx.model.as_deref()) {
2276 Some(inner) => inner,
2277 None => capability.as_ref(),
2278 };
2279 let effective_id = effective.id();
2280
2281 if let Some(contribution) = effective
2283 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2284 .await
2285 {
2286 system_prompt_attributions.push(SystemPromptAttribution {
2287 capability_id: cap_id.to_string(),
2288 content: contribution.clone(),
2289 });
2290 system_prompt_parts.push(contribution);
2291 }
2292
2293 tools.extend(effective.tools_with_config(&cap_config.config));
2295 tool_definition_hooks
2296 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2297 tool_call_hooks.extend(effective.tool_call_hooks());
2298 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2300 let cap_category = effective.category();
2305 for def in effective.tool_definitions() {
2306 let def = match (def.category(), cap_category) {
2307 (None, Some(cat)) => def.with_category(cat),
2308 _ => def,
2309 }
2310 .with_capability_attribution(cap_id, Some(capability.name()));
2311 tool_definitions.push(def);
2312 }
2313
2314 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2322 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2323 {
2324 let threshold = cap_config
2326 .config
2327 .get("threshold")
2328 .and_then(|v| v.as_u64())
2329 .map(|v| v as usize)
2330 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2331 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2332 enabled: true,
2333 threshold,
2334 });
2335 }
2336
2337 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2338 let strategy = cap_config
2339 .config
2340 .get("strategy")
2341 .and_then(|v| v.as_str())
2342 .map(|value| match value {
2343 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2344 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2345 })
2346 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2347 let gemini_cached_content = cap_config
2348 .config
2349 .get("gemini_cached_content")
2350 .and_then(|v| v.as_str())
2351 .map(str::to_string);
2352 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2353 enabled: true,
2354 strategy,
2355 gemini_cached_content,
2356 });
2357 }
2358
2359 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2360 let server_tools =
2361 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2362 if !server_tools.is_empty() {
2363 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2364 server_tools,
2365 ..Default::default()
2366 });
2367 }
2368 }
2369
2370 mounts.extend(effective.mounts());
2372
2373 mcp_servers = merge_scoped_mcp_servers(
2374 &mcp_servers,
2375 &effective.mcp_servers_with_config(&cap_config.config),
2376 );
2377
2378 for skill in effective.contribute_skills() {
2382 mounts.push(skill.to_mount(cap_id));
2383 }
2384
2385 if let Some(provider) = effective.message_filter_provider() {
2387 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2388 message_filter_providers.push((provider, config));
2389 }
2390
2391 applied_ids.push(cap_id.to_string());
2392 }
2393 }
2394
2395 if !applied_ids
2407 .iter()
2408 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2409 && tool_definitions
2410 .iter()
2411 .any(|def| def.hints().supports_background == Some(true))
2412 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2413 && bg_cap.status() == CapabilityStatus::Available
2414 {
2415 tools.extend(bg_cap.tools());
2416 let cap_category = bg_cap.category();
2417 for def in bg_cap.tool_definitions() {
2418 let def = match (def.category(), cap_category) {
2419 (None, Some(cat)) => def.with_category(cat),
2420 _ => def,
2421 }
2422 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2423 tool_definitions.push(def);
2424 }
2425 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2426 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2427 }
2428
2429 tool_call_hooks.extend(narration_hooks);
2433
2434 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2436
2437 CollectedCapabilities {
2438 system_prompt_parts,
2439 system_prompt_attributions,
2440 tools,
2441 tool_definitions,
2442 mounts,
2443 message_filter_providers,
2444 applied_ids,
2445 tool_search,
2446 prompt_cache,
2447 openrouter_routing,
2448 tool_definition_hooks,
2449 tool_call_hooks,
2450 mcp_servers,
2451 }
2452}
2453
2454pub struct AppliedCapabilities {
2460 pub runtime_agent: RuntimeAgent,
2462 pub tool_registry: ToolRegistry,
2464 pub applied_ids: Vec<String>,
2466}
2467
2468pub async fn apply_capabilities(
2505 base_runtime_agent: RuntimeAgent,
2506 capability_ids: &[String],
2507 registry: &CapabilityRegistry,
2508 ctx: &SystemPromptContext,
2509) -> AppliedCapabilities {
2510 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2511
2512 let final_system_prompt = compose_system_prompt(
2514 &base_runtime_agent.system_prompt,
2515 collected.system_prompt_prefix().as_deref(),
2516 );
2517
2518 let mut tool_registry = ToolRegistry::new();
2520 for tool in collected.tools {
2521 tool_registry.register_boxed(tool);
2522 }
2523
2524 let mut tools = collected.tool_definitions;
2526 for hook in &collected.tool_definition_hooks {
2527 tools = hook.transform(tools);
2528 }
2529
2530 let runtime_agent = RuntimeAgent {
2531 system_prompt: final_system_prompt,
2532 model: base_runtime_agent.model,
2533 tools,
2534 max_iterations: base_runtime_agent.max_iterations,
2535 temperature: base_runtime_agent.temperature,
2536 max_tokens: base_runtime_agent.max_tokens,
2537 tool_search: collected.tool_search,
2538 prompt_cache: collected.prompt_cache,
2539 openrouter_routing: collected.openrouter_routing,
2540 network_access: base_runtime_agent.network_access,
2541 parallel_tool_calls: base_runtime_agent.parallel_tool_calls,
2542 };
2543
2544 AppliedCapabilities {
2545 runtime_agent,
2546 tool_registry,
2547 applied_ids: collected.applied_ids,
2548 }
2549}
2550
2551#[cfg(test)]
2556mod tests {
2557 use super::*;
2558 use crate::typed_id::SessionId;
2559 use std::collections::BTreeSet;
2560 use uuid::Uuid;
2561
2562 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2564
2565 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2566 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2567 }
2568
2569 fn test_ctx() -> SystemPromptContext {
2571 SystemPromptContext::without_file_store(SessionId::new())
2572 }
2573
2574 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2576 let mut ids = [
2577 "agent_instructions",
2578 "human_intent",
2579 "budgeting",
2580 "self_budget",
2581 "noop",
2582 "current_time",
2583 "research",
2584 "platform_management",
2585 "session_file_system",
2586 "session_storage",
2587 "session",
2588 "session_sql_database",
2589 "test_math",
2590 "test_weather",
2591 "stateless_todo_list",
2592 "web_fetch",
2593 "bashkit_shell",
2594 "background_execution",
2595 "session_schedule",
2596 "btw",
2597 "infinity_context",
2598 "compaction",
2599 "memory",
2600 "message_metadata",
2601 "openai_tool_search",
2602 "claude_tool_search",
2603 "tool_search",
2604 "auto_tool_search",
2605 "prompt_caching",
2606 "session_tasks",
2607 "skills",
2608 "subagents",
2609 "system_commands",
2610 "sample_data",
2611 "data_knowledge",
2612 "knowledge_base",
2613 "knowledge_index",
2614 "tool_output_persistence",
2615 "tool_output_distillation",
2616 "fake_warehouse",
2617 "fake_aws",
2618 "fake_crm",
2619 "fake_financial",
2620 "loop_detection",
2621 "error_disclosure",
2622 "prompt_canary_guardrail",
2623 "guardrails",
2624 "user_hooks",
2625 "model_scout",
2626 "openrouter_workspace",
2627 "openrouter_server_tools",
2628 ]
2629 .into_iter()
2630 .collect::<BTreeSet<_>>();
2631 if cfg!(feature = "ui-capabilities") {
2632 ids.insert("openui");
2633 ids.insert("a2ui");
2634 }
2635 ids
2636 }
2637
2638 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2640 let mut ids = expected_core_builtin_ids();
2641 ids.insert("agent_handoff");
2642 ids.insert("a2a_agent_delegation");
2643 ids
2644 }
2645
2646 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2647 registry.capabilities.keys().map(String::as_str).collect()
2648 }
2649
2650 #[test]
2660 fn test_capability_registry_with_builtins_dev() {
2661 let _lock = lock_env();
2663 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2664 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2665 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2666 assert!(registry.has("agent_handoff"));
2667 assert!(registry.has("a2a_agent_delegation"));
2668 }
2669
2670 #[test]
2671 fn test_capability_registry_with_builtins_prod() {
2672 let _lock = lock_env();
2674 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2675 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2676 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2677 assert!(!registry.has("docker_container"));
2679 assert!(!registry.has("agent_handoff"));
2680 assert!(!registry.has("a2a_agent_delegation"));
2681 }
2682
2683 #[test]
2684 fn test_agent_delegation_enabled_by_env_in_prod() {
2685 let _lock = lock_env();
2687 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2688 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2689 assert!(registry.has("agent_handoff"));
2690 assert!(registry.has("a2a_agent_delegation"));
2691 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2692 }
2693
2694 #[test]
2695 fn test_agent_delegation_disabled_by_env_in_dev() {
2696 let _lock = lock_env();
2698 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2699 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2700 assert!(!registry.has("agent_handoff"));
2701 assert!(!registry.has("a2a_agent_delegation"));
2702 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2703 }
2704
2705 #[test]
2706 fn test_capability_registry_get() {
2707 let registry = CapabilityRegistry::with_builtins();
2708
2709 let noop = registry.get("noop").unwrap();
2710 assert_eq!(noop.id(), "noop");
2711 assert_eq!(noop.name(), "No-Op");
2712 assert_eq!(noop.status(), CapabilityStatus::Available);
2713 }
2714
2715 #[test]
2716 fn test_capability_registry_blueprint_with_capability() {
2717 struct BlueprintProviderCapability;
2718
2719 impl Capability for BlueprintProviderCapability {
2720 fn id(&self) -> &str {
2721 "blueprint_provider"
2722 }
2723 fn name(&self) -> &str {
2724 "Blueprint Provider"
2725 }
2726 fn description(&self) -> &str {
2727 "Capability that provides a blueprint for tests"
2728 }
2729 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2730 vec![AgentBlueprint {
2731 id: "test_blueprint",
2732 name: "Test Blueprint",
2733 description: "Blueprint for capability registry tests",
2734 model: BlueprintModel::Inherit,
2735 system_prompt: "Test prompt",
2736 tools: vec![],
2737 max_turns: None,
2738 config_schema: None,
2739 }]
2740 }
2741 }
2742
2743 let mut registry = CapabilityRegistry::new();
2744 registry.register(BlueprintProviderCapability);
2745
2746 let (capability_id, blueprint) = registry
2747 .blueprint_with_capability("test_blueprint")
2748 .expect("blueprint should resolve with capability id");
2749 assert_eq!(capability_id, "blueprint_provider");
2750 assert_eq!(blueprint.id, "test_blueprint");
2751 }
2752
2753 #[test]
2754 fn test_capability_registry_builder() {
2755 let registry = CapabilityRegistry::builder()
2756 .capability(NoopCapability)
2757 .capability(CurrentTimeCapability)
2758 .build();
2759
2760 assert!(registry.has("noop"));
2761 assert!(registry.has("current_time"));
2762 assert_eq!(registry.len(), 2);
2763 }
2764
2765 #[test]
2766 fn test_capability_status() {
2767 let registry = CapabilityRegistry::with_builtins();
2768
2769 let current_time = registry.get("current_time").unwrap();
2770 assert_eq!(current_time.status(), CapabilityStatus::Available);
2771
2772 let research = registry.get("research").unwrap();
2773 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2774 }
2775
2776 #[test]
2777 fn test_capability_icons_and_categories() {
2778 let registry = CapabilityRegistry::with_builtins();
2779
2780 let noop = registry.get("noop").unwrap();
2781 assert_eq!(noop.icon(), Some("circle-off"));
2782 assert_eq!(noop.category(), Some("Testing"));
2783
2784 let current_time = registry.get("current_time").unwrap();
2785 assert_eq!(current_time.icon(), Some("clock"));
2786 assert_eq!(current_time.category(), Some("Core"));
2787 }
2788
2789 #[test]
2790 fn test_system_prompt_preview_default_delegates_to_addition() {
2791 let registry = CapabilityRegistry::with_builtins();
2792
2793 let test_math = registry.get("test_math").unwrap();
2795 assert_eq!(
2796 test_math.system_prompt_preview().as_deref(),
2797 test_math.system_prompt_addition()
2798 );
2799
2800 let current_time = registry.get("current_time").unwrap();
2802 assert!(current_time.system_prompt_preview().is_none());
2803 assert!(current_time.system_prompt_addition().is_none());
2804 }
2805
2806 #[test]
2807 fn test_system_prompt_preview_dynamic_capability() {
2808 let registry = CapabilityRegistry::with_builtins();
2809 let cap = registry.get("agent_instructions").unwrap();
2810
2811 assert!(cap.system_prompt_addition().is_none());
2813 assert!(cap.system_prompt_preview().is_some());
2814 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2815 }
2816
2817 #[tokio::test]
2822 async fn test_apply_capabilities_empty() {
2823 let registry = CapabilityRegistry::with_builtins();
2824 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2825
2826 let applied =
2827 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
2828
2829 assert_eq!(
2830 applied.runtime_agent.system_prompt,
2831 base_runtime_agent.system_prompt
2832 );
2833 assert!(applied.tool_registry.is_empty());
2834 assert!(applied.applied_ids.is_empty());
2835 }
2836
2837 #[tokio::test]
2838 async fn test_apply_capabilities_noop() {
2839 let registry = CapabilityRegistry::with_builtins();
2840 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2841
2842 let applied = apply_capabilities(
2843 base_runtime_agent.clone(),
2844 &["noop".to_string()],
2845 ®istry,
2846 &test_ctx(),
2847 )
2848 .await;
2849
2850 assert_eq!(
2852 applied.runtime_agent.system_prompt,
2853 base_runtime_agent.system_prompt
2854 );
2855 assert!(applied.tool_registry.is_empty());
2856 assert_eq!(applied.applied_ids, vec!["noop"]);
2857 }
2858
2859 #[tokio::test]
2860 async fn test_apply_capabilities_current_time() {
2861 let registry = CapabilityRegistry::with_builtins();
2862 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2863
2864 let applied = apply_capabilities(
2865 base_runtime_agent.clone(),
2866 &["current_time".to_string()],
2867 ®istry,
2868 &test_ctx(),
2869 )
2870 .await;
2871
2872 assert_eq!(
2874 applied.runtime_agent.system_prompt,
2875 base_runtime_agent.system_prompt
2876 );
2877 assert!(applied.tool_registry.has("get_current_time"));
2878 assert_eq!(applied.tool_registry.len(), 1);
2879 assert_eq!(applied.applied_ids, vec!["current_time"]);
2880 }
2881
2882 #[tokio::test]
2883 async fn test_apply_capabilities_skips_coming_soon() {
2884 let registry = CapabilityRegistry::with_builtins();
2885 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2886
2887 let applied = apply_capabilities(
2889 base_runtime_agent.clone(),
2890 &["research".to_string()],
2891 ®istry,
2892 &test_ctx(),
2893 )
2894 .await;
2895
2896 assert_eq!(
2898 applied.runtime_agent.system_prompt,
2899 base_runtime_agent.system_prompt
2900 );
2901 assert!(applied.applied_ids.is_empty()); }
2903
2904 #[tokio::test]
2905 async fn test_apply_capabilities_multiple() {
2906 let registry = CapabilityRegistry::with_builtins();
2907 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2908
2909 let applied = apply_capabilities(
2910 base_runtime_agent.clone(),
2911 &["noop".to_string(), "current_time".to_string()],
2912 ®istry,
2913 &test_ctx(),
2914 )
2915 .await;
2916
2917 assert!(applied.tool_registry.has("get_current_time"));
2918 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
2919 }
2920
2921 #[tokio::test]
2922 async fn test_apply_capabilities_preserves_order() {
2923 let registry = CapabilityRegistry::with_builtins();
2924 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
2925
2926 let applied = apply_capabilities(
2928 base_runtime_agent,
2929 &["current_time".to_string(), "noop".to_string()],
2930 ®istry,
2931 &test_ctx(),
2932 )
2933 .await;
2934
2935 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
2936 }
2937
2938 #[tokio::test]
2939 async fn test_apply_capabilities_test_math() {
2940 let registry = CapabilityRegistry::with_builtins();
2941 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2942
2943 let applied = apply_capabilities(
2944 base_runtime_agent.clone(),
2945 &["test_math".to_string()],
2946 ®istry,
2947 &test_ctx(),
2948 )
2949 .await;
2950
2951 assert!(
2953 !applied
2954 .runtime_agent
2955 .system_prompt
2956 .contains("<capability id=\"test_math\">")
2957 );
2958 assert!(
2960 applied
2961 .runtime_agent
2962 .system_prompt
2963 .contains("You are a helpful assistant.")
2964 );
2965 assert!(applied.tool_registry.has("add"));
2966 assert!(applied.tool_registry.has("subtract"));
2967 assert!(applied.tool_registry.has("multiply"));
2968 assert!(applied.tool_registry.has("divide"));
2969 assert_eq!(applied.tool_registry.len(), 4);
2970 }
2971
2972 #[tokio::test]
2973 async fn test_apply_capabilities_test_weather() {
2974 let registry = CapabilityRegistry::with_builtins();
2975 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2976
2977 let applied = apply_capabilities(
2978 base_runtime_agent.clone(),
2979 &["test_weather".to_string()],
2980 ®istry,
2981 &test_ctx(),
2982 )
2983 .await;
2984
2985 assert!(
2987 !applied
2988 .runtime_agent
2989 .system_prompt
2990 .contains("<capability id=\"test_weather\">")
2991 );
2992 assert!(applied.tool_registry.has("get_weather"));
2993 assert!(applied.tool_registry.has("get_forecast"));
2994 assert_eq!(applied.tool_registry.len(), 2);
2995 }
2996
2997 #[tokio::test]
2998 async fn test_apply_capabilities_test_math_and_test_weather() {
2999 let registry = CapabilityRegistry::with_builtins();
3000 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3001
3002 let applied = apply_capabilities(
3003 base_runtime_agent.clone(),
3004 &["test_math".to_string(), "test_weather".to_string()],
3005 ®istry,
3006 &test_ctx(),
3007 )
3008 .await;
3009
3010 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3013 assert!(applied.tool_registry.has("get_weather"));
3014 }
3015
3016 #[tokio::test]
3017 async fn test_apply_capabilities_stateless_todo_list() {
3018 let registry = CapabilityRegistry::with_builtins();
3019 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3020
3021 let applied = apply_capabilities(
3022 base_runtime_agent.clone(),
3023 &["stateless_todo_list".to_string()],
3024 ®istry,
3025 &test_ctx(),
3026 )
3027 .await;
3028
3029 assert!(
3031 applied
3032 .runtime_agent
3033 .system_prompt
3034 .contains("Task Management")
3035 );
3036 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3037 assert!(applied.tool_registry.has("write_todos"));
3038 assert_eq!(applied.tool_registry.len(), 1);
3039 }
3040
3041 #[tokio::test]
3042 async fn test_apply_capabilities_web_fetch() {
3043 let registry = CapabilityRegistry::with_builtins();
3044 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3045
3046 let applied = apply_capabilities(
3047 base_runtime_agent.clone(),
3048 &["web_fetch".to_string()],
3049 ®istry,
3050 &test_ctx(),
3051 )
3052 .await;
3053
3054 assert!(
3056 applied
3057 .runtime_agent
3058 .system_prompt
3059 .contains(&base_runtime_agent.system_prompt)
3060 );
3061 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3062 assert!(applied.tool_registry.has("web_fetch"));
3063 assert_eq!(applied.tool_registry.len(), 1);
3064 }
3065
3066 #[tokio::test]
3071 async fn test_xml_tags_wrap_capability_prompts() {
3072 let registry = CapabilityRegistry::with_builtins();
3073 let collected =
3074 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3075 .await;
3076
3077 assert_eq!(collected.system_prompt_parts.len(), 1);
3078 let part = &collected.system_prompt_parts[0];
3079 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3080 assert!(part.ends_with("</capability>"));
3081 assert!(part.contains("Task Management"));
3082 }
3083
3084 #[tokio::test]
3085 async fn test_xml_tags_multiple_capabilities() {
3086 let registry = CapabilityRegistry::with_builtins();
3087 let collected = collect_capabilities(
3088 &[
3089 "stateless_todo_list".to_string(),
3090 "session_schedule".to_string(),
3091 ],
3092 ®istry,
3093 &test_ctx(),
3094 )
3095 .await;
3096
3097 assert_eq!(collected.system_prompt_parts.len(), 2);
3098 assert!(
3099 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3100 );
3101 assert!(
3102 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3103 );
3104
3105 let prefix = collected.system_prompt_prefix().unwrap();
3106 assert!(prefix.contains("</capability>\n\n<capability"));
3108 }
3109
3110 #[tokio::test]
3111 async fn test_xml_tags_system_prompt_wrapping() {
3112 let registry = CapabilityRegistry::with_builtins();
3113 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3114
3115 let applied = apply_capabilities(
3116 base,
3117 &["stateless_todo_list".to_string()],
3118 ®istry,
3119 &test_ctx(),
3120 )
3121 .await;
3122
3123 let prompt = &applied.runtime_agent.system_prompt;
3124 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3125 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3127 assert!(prompt.contains("</capability>"));
3128 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3130 }
3131
3132 #[tokio::test]
3133 async fn test_no_xml_wrapping_without_capabilities() {
3134 let registry = CapabilityRegistry::with_builtins();
3135 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3136
3137 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3138
3139 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3141 assert!(
3142 !applied
3143 .runtime_agent
3144 .system_prompt
3145 .contains("<system-prompt>")
3146 );
3147 }
3148
3149 #[tokio::test]
3150 async fn test_no_xml_wrapping_for_noop_capability() {
3151 let registry = CapabilityRegistry::with_builtins();
3152 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3153
3154 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3156
3157 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3158 assert!(
3159 !applied
3160 .runtime_agent
3161 .system_prompt
3162 .contains("<system-prompt>")
3163 );
3164 }
3165
3166 #[tokio::test]
3171 async fn test_collect_capabilities_includes_mounts() {
3172 let registry = CapabilityRegistry::with_builtins();
3173
3174 let collected =
3175 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3176
3177 assert!(!collected.mounts.is_empty());
3178 assert_eq!(collected.mounts.len(), 1);
3179 assert_eq!(collected.mounts[0].path, "/samples");
3180 assert!(collected.mounts[0].is_readonly());
3181 }
3182
3183 #[tokio::test]
3184 async fn test_collect_capabilities_empty_mounts_by_default() {
3185 let registry = CapabilityRegistry::with_builtins();
3186
3187 let collected =
3189 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3190
3191 assert!(collected.mounts.is_empty());
3192 }
3193
3194 #[tokio::test]
3195 async fn test_collect_capabilities_combines_mounts() {
3196 let registry = CapabilityRegistry::with_builtins();
3197
3198 let collected = collect_capabilities(
3201 &["sample_data".to_string(), "current_time".to_string()],
3202 ®istry,
3203 &test_ctx(),
3204 )
3205 .await;
3206
3207 assert_eq!(collected.mounts.len(), 1);
3208 assert!(
3210 collected
3211 .applied_ids
3212 .iter()
3213 .any(|id| id == "session_file_system")
3214 );
3215 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3216 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3217 }
3218
3219 #[test]
3220 fn test_sample_data_capability() {
3221 let registry = CapabilityRegistry::with_builtins();
3222 let cap = registry.get("sample_data").unwrap();
3223
3224 assert_eq!(cap.id(), "sample_data");
3225 assert_eq!(cap.name(), "Sample Data");
3226 assert_eq!(cap.status(), CapabilityStatus::Available);
3227
3228 assert!(cap.system_prompt_addition().is_some());
3230 assert!(cap.tools().is_empty());
3231
3232 assert!(!cap.mounts().is_empty());
3234 }
3235
3236 #[test]
3241 fn test_resolve_dependencies_empty() {
3242 let registry = CapabilityRegistry::with_builtins();
3243
3244 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3245
3246 assert!(resolved.resolved_ids.is_empty());
3247 assert!(resolved.added_as_dependencies.is_empty());
3248 assert!(resolved.user_selected.is_empty());
3249 }
3250
3251 #[test]
3252 fn test_resolve_dependencies_no_deps() {
3253 let registry = CapabilityRegistry::with_builtins();
3254
3255 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3257
3258 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3259 assert!(resolved.added_as_dependencies.is_empty());
3260 }
3261
3262 #[test]
3263 fn test_resolve_dependencies_with_deps() {
3264 let registry = CapabilityRegistry::with_builtins();
3265
3266 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3268
3269 assert_eq!(resolved.resolved_ids.len(), 2);
3271 let fs_pos = resolved
3272 .resolved_ids
3273 .iter()
3274 .position(|id| id == "session_file_system")
3275 .unwrap();
3276 let sd_pos = resolved
3277 .resolved_ids
3278 .iter()
3279 .position(|id| id == "sample_data")
3280 .unwrap();
3281 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3282
3283 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3285 }
3286
3287 #[test]
3288 fn test_resolve_dependencies_already_selected() {
3289 let registry = CapabilityRegistry::with_builtins();
3290
3291 let resolved = resolve_dependencies(
3293 &["session_file_system".to_string(), "sample_data".to_string()],
3294 ®istry,
3295 )
3296 .unwrap();
3297
3298 assert_eq!(resolved.resolved_ids.len(), 2);
3299 assert!(resolved.added_as_dependencies.is_empty());
3301 }
3302
3303 #[test]
3304 fn test_resolve_dependencies_preserves_order() {
3305 let registry = CapabilityRegistry::with_builtins();
3306
3307 let resolved =
3309 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
3310 .unwrap();
3311
3312 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
3313 }
3314
3315 #[test]
3316 fn test_resolve_dependencies_unknown_capability() {
3317 let registry = CapabilityRegistry::with_builtins();
3318
3319 let resolved =
3321 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
3322
3323 assert!(resolved.resolved_ids.is_empty());
3324 }
3325
3326 #[test]
3327 fn test_get_dependencies() {
3328 let registry = CapabilityRegistry::with_builtins();
3329
3330 let deps = get_dependencies("sample_data", ®istry);
3332 assert_eq!(deps, vec!["session_file_system"]);
3333
3334 let deps = get_dependencies("current_time", ®istry);
3336 assert!(deps.is_empty());
3337
3338 let deps = get_dependencies("unknown", ®istry);
3340 assert!(deps.is_empty());
3341 }
3342
3343 #[test]
3344 fn test_sample_data_has_dependency() {
3345 let registry = CapabilityRegistry::with_builtins();
3346 let cap = registry.get("sample_data").unwrap();
3347
3348 let deps = cap.dependencies();
3349 assert_eq!(deps.len(), 1);
3350 assert_eq!(deps[0], "session_file_system");
3351 }
3352
3353 #[test]
3354 fn test_noop_has_no_dependencies() {
3355 let registry = CapabilityRegistry::with_builtins();
3356 let cap = registry.get("noop").unwrap();
3357
3358 assert!(cap.dependencies().is_empty());
3359 }
3360
3361 #[test]
3365 fn test_circular_dependency_error() {
3366 struct CapA;
3368 struct CapB;
3369
3370 impl Capability for CapA {
3371 fn id(&self) -> &str {
3372 "test_cap_a"
3373 }
3374 fn name(&self) -> &str {
3375 "Test A"
3376 }
3377 fn description(&self) -> &str {
3378 "Test capability A"
3379 }
3380 fn dependencies(&self) -> Vec<&'static str> {
3381 vec!["test_cap_b"]
3382 }
3383 }
3384
3385 impl Capability for CapB {
3386 fn id(&self) -> &str {
3387 "test_cap_b"
3388 }
3389 fn name(&self) -> &str {
3390 "Test B"
3391 }
3392 fn description(&self) -> &str {
3393 "Test capability B"
3394 }
3395 fn dependencies(&self) -> Vec<&'static str> {
3396 vec!["test_cap_a"]
3397 }
3398 }
3399
3400 let mut registry = CapabilityRegistry::new();
3401 registry.register(CapA);
3402 registry.register(CapB);
3403
3404 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
3405
3406 assert!(result.is_err());
3407 match result.unwrap_err() {
3408 DependencyError::CircularDependency { capability_id, .. } => {
3409 assert_eq!(capability_id, "test_cap_a");
3410 }
3411 _ => panic!("Expected CircularDependency error"),
3412 }
3413 }
3414
3415 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
3420
3421 struct FilterTestCapability {
3423 priority: i32,
3424 }
3425
3426 impl Capability for FilterTestCapability {
3427 fn id(&self) -> &str {
3428 "filter_test"
3429 }
3430 fn name(&self) -> &str {
3431 "Filter Test"
3432 }
3433 fn description(&self) -> &str {
3434 "Test capability with message filter"
3435 }
3436 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3437 Some(Arc::new(FilterTestProvider {
3438 priority: self.priority,
3439 }))
3440 }
3441 }
3442
3443 struct FilterTestProvider {
3444 priority: i32,
3445 }
3446
3447 impl MessageFilterProvider for FilterTestProvider {
3448 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
3449 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
3451 query
3452 .filters
3453 .push(MessageFilter::Search(search.to_string()));
3454 }
3455 }
3456
3457 fn priority(&self) -> i32 {
3458 self.priority
3459 }
3460 }
3461
3462 #[tokio::test]
3463 async fn test_collect_capabilities_with_configs_no_filter_providers() {
3464 let registry = CapabilityRegistry::with_builtins();
3465 let configs = vec![AgentCapabilityConfig {
3466 capability_ref: CapabilityId::new("current_time"),
3467 config: serde_json::json!({}),
3468 }];
3469
3470 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3471
3472 assert!(collected.message_filter_providers.is_empty());
3473 assert!(!collected.has_message_filters());
3474 }
3475
3476 #[tokio::test]
3477 async fn test_collect_capabilities_with_configs_with_filter_provider() {
3478 let mut registry = CapabilityRegistry::new();
3479 registry.register(FilterTestCapability { priority: 0 });
3480
3481 let configs = vec![AgentCapabilityConfig {
3482 capability_ref: CapabilityId::new("filter_test"),
3483 config: serde_json::json!({ "search": "hello" }),
3484 }];
3485
3486 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3487
3488 assert_eq!(collected.message_filter_providers.len(), 1);
3489 assert!(collected.has_message_filters());
3490 }
3491
3492 #[tokio::test]
3493 async fn test_collect_capabilities_with_configs_filter_priority_order() {
3494 struct HighPriorityCapability;
3496 struct LowPriorityCapability;
3497
3498 impl Capability for HighPriorityCapability {
3499 fn id(&self) -> &str {
3500 "high_priority"
3501 }
3502 fn name(&self) -> &str {
3503 "High Priority"
3504 }
3505 fn description(&self) -> &str {
3506 "Test"
3507 }
3508 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3509 Some(Arc::new(FilterTestProvider { priority: 10 }))
3510 }
3511 }
3512
3513 impl Capability for LowPriorityCapability {
3514 fn id(&self) -> &str {
3515 "low_priority"
3516 }
3517 fn name(&self) -> &str {
3518 "Low Priority"
3519 }
3520 fn description(&self) -> &str {
3521 "Test"
3522 }
3523 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3524 Some(Arc::new(FilterTestProvider { priority: -5 }))
3525 }
3526 }
3527
3528 let mut registry = CapabilityRegistry::new();
3529 registry.register(HighPriorityCapability);
3530 registry.register(LowPriorityCapability);
3531
3532 let configs = vec![
3534 AgentCapabilityConfig {
3535 capability_ref: CapabilityId::new("high_priority"),
3536 config: serde_json::json!({}),
3537 },
3538 AgentCapabilityConfig {
3539 capability_ref: CapabilityId::new("low_priority"),
3540 config: serde_json::json!({}),
3541 },
3542 ];
3543
3544 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3545
3546 assert_eq!(collected.message_filter_providers.len(), 2);
3548 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
3549 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
3550 }
3551
3552 #[tokio::test]
3553 async fn test_collected_capabilities_apply_message_filters() {
3554 let mut registry = CapabilityRegistry::new();
3555 registry.register(FilterTestCapability { priority: 0 });
3556
3557 let configs = vec![AgentCapabilityConfig {
3558 capability_ref: CapabilityId::new("filter_test"),
3559 config: serde_json::json!({ "search": "test_query" }),
3560 }];
3561
3562 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3563
3564 let session_id: SessionId = Uuid::now_v7().into();
3566 let mut query = MessageQuery::new(session_id);
3567
3568 collected.apply_message_filters(&mut query);
3569
3570 assert_eq!(query.filters.len(), 1);
3572 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3573 }
3574
3575 #[tokio::test]
3576 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3577 struct SearchCapability {
3578 id: &'static str,
3579 search_term: &'static str,
3580 priority: i32,
3581 }
3582
3583 struct SearchProvider {
3584 search_term: &'static str,
3585 priority: i32,
3586 }
3587
3588 impl MessageFilterProvider for SearchProvider {
3589 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3590 query
3591 .filters
3592 .push(MessageFilter::Search(self.search_term.to_string()));
3593 }
3594
3595 fn priority(&self) -> i32 {
3596 self.priority
3597 }
3598 }
3599
3600 impl Capability for SearchCapability {
3601 fn id(&self) -> &str {
3602 self.id
3603 }
3604 fn name(&self) -> &str {
3605 "Search"
3606 }
3607 fn description(&self) -> &str {
3608 "Test"
3609 }
3610 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3611 Some(Arc::new(SearchProvider {
3612 search_term: self.search_term,
3613 priority: self.priority,
3614 }))
3615 }
3616 }
3617
3618 let mut registry = CapabilityRegistry::new();
3619 registry.register(SearchCapability {
3620 id: "cap_a",
3621 search_term: "alpha",
3622 priority: 5,
3623 });
3624 registry.register(SearchCapability {
3625 id: "cap_b",
3626 search_term: "beta",
3627 priority: 1,
3628 });
3629 registry.register(SearchCapability {
3630 id: "cap_c",
3631 search_term: "gamma",
3632 priority: 10,
3633 });
3634
3635 let configs = vec![
3636 AgentCapabilityConfig {
3637 capability_ref: CapabilityId::new("cap_a"),
3638 config: serde_json::json!({}),
3639 },
3640 AgentCapabilityConfig {
3641 capability_ref: CapabilityId::new("cap_b"),
3642 config: serde_json::json!({}),
3643 },
3644 AgentCapabilityConfig {
3645 capability_ref: CapabilityId::new("cap_c"),
3646 config: serde_json::json!({}),
3647 },
3648 ];
3649
3650 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3651
3652 let session_id: SessionId = Uuid::now_v7().into();
3653 let mut query = MessageQuery::new(session_id);
3654
3655 collected.apply_message_filters(&mut query);
3656
3657 assert_eq!(query.filters.len(), 3);
3659 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3660 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3661 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3662 }
3663
3664 #[test]
3665 fn test_capability_without_message_filter_returns_none() {
3666 let registry = CapabilityRegistry::with_builtins();
3667
3668 let noop = registry.get("noop").unwrap();
3669 assert!(noop.message_filter_provider().is_none());
3670
3671 let current_time = registry.get("current_time").unwrap();
3672 assert!(current_time.message_filter_provider().is_none());
3673 }
3674
3675 #[tokio::test]
3676 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3677 let mut registry = CapabilityRegistry::new();
3678 registry.register(FilterTestCapability { priority: 0 });
3679
3680 let test_config = serde_json::json!({
3681 "search": "custom_search",
3682 "extra_field": 42
3683 });
3684
3685 let configs = vec![AgentCapabilityConfig {
3686 capability_ref: CapabilityId::new("filter_test"),
3687 config: test_config.clone(),
3688 }];
3689
3690 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3691
3692 assert_eq!(collected.message_filter_providers.len(), 1);
3694 let (_, stored_config) = &collected.message_filter_providers[0];
3695 assert_eq!(*stored_config, test_config);
3696 }
3697
3698 #[test]
3703 fn test_collect_message_filters_only_collects_filters() {
3704 let mut registry = CapabilityRegistry::new();
3705 registry.register(FilterTestCapability { priority: 0 });
3706
3707 let configs = vec![AgentCapabilityConfig {
3708 capability_ref: CapabilityId::new("filter_test"),
3709 config: serde_json::json!({ "search": "test_query" }),
3710 }];
3711
3712 let collected = collect_message_filters_only(&configs, ®istry);
3713
3714 let session_id: SessionId = Uuid::now_v7().into();
3715 let mut query = MessageQuery::new(session_id);
3716 collected.apply_message_filters(&mut query);
3717
3718 assert_eq!(query.filters.len(), 1);
3719 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3720 }
3721
3722 #[test]
3723 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
3724 let base = serde_json::json!({ "context_budget_tokens": 1000 });
3725
3726 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
3728 assert_eq!(with["compaction_active"], serde_json::json!(true));
3729 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
3730
3731 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
3732 assert!(without.get("compaction_active").is_none());
3733
3734 let other = message_filter_config_for("other", &base, true);
3736 assert!(other.get("compaction_active").is_none());
3737
3738 let null_base = message_filter_config_for(
3740 INFINITY_CONTEXT_CAPABILITY_ID,
3741 &serde_json::Value::Null,
3742 true,
3743 );
3744 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
3745 }
3746
3747 #[test]
3748 fn test_infinity_context_defers_to_compaction_end_to_end() {
3749 use crate::message::Message;
3750
3751 let mut registry = CapabilityRegistry::new();
3752 registry.register(InfinityContextCapability);
3753 registry.register(CompactionCapability);
3754
3755 let tight = serde_json::json!({
3756 "context_budget_tokens": 1,
3757 "min_recent_messages": 1
3758 });
3759
3760 let solo = vec![AgentCapabilityConfig {
3762 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3763 config: tight.clone(),
3764 }];
3765 let mut messages = vec![
3766 Message::user("task"),
3767 Message::assistant("old ".repeat(400)),
3768 Message::user("recent"),
3769 ];
3770 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
3771 assert!(
3772 messages
3773 .iter()
3774 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
3775 "infinity context alone should trim and notice"
3776 );
3777
3778 let both = vec![
3780 AgentCapabilityConfig {
3781 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3782 config: tight,
3783 },
3784 AgentCapabilityConfig {
3785 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3786 config: serde_json::json!({}),
3787 },
3788 ];
3789 let mut messages = vec![
3790 Message::user("task"),
3791 Message::assistant("old ".repeat(400)),
3792 Message::user("recent"),
3793 ];
3794 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
3795 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
3796 assert!(
3797 messages
3798 .iter()
3799 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
3800 "no hidden-history notice when compaction is the active reducer"
3801 );
3802 }
3803
3804 #[test]
3805 fn test_compaction_is_enabled_detects_compaction() {
3806 let mut registry = CapabilityRegistry::new();
3807 registry.register(CompactionCapability);
3808
3809 let with_compaction = vec![AgentCapabilityConfig {
3810 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3811 config: serde_json::json!({}),
3812 }];
3813 assert!(compaction_is_enabled(&with_compaction, ®istry));
3814
3815 let without = vec![AgentCapabilityConfig {
3816 capability_ref: CapabilityId::new("current_time"),
3817 config: serde_json::json!({}),
3818 }];
3819 assert!(!compaction_is_enabled(&without, ®istry));
3820 }
3821
3822 #[test]
3823 fn test_collect_message_filters_only_skips_unknown_capabilities() {
3824 let registry = CapabilityRegistry::new();
3825
3826 let configs = vec![AgentCapabilityConfig {
3827 capability_ref: CapabilityId::new("nonexistent"),
3828 config: serde_json::json!({}),
3829 }];
3830
3831 let collected = collect_message_filters_only(&configs, ®istry);
3832 assert!(collected.message_filter_providers.is_empty());
3833 }
3834
3835 #[test]
3836 fn test_collect_message_filters_only_preserves_priority_order() {
3837 struct PriorityFilterCap {
3838 id: &'static str,
3839 search_term: &'static str,
3840 priority: i32,
3841 }
3842
3843 struct PriorityFilterProvider {
3844 search_term: &'static str,
3845 priority: i32,
3846 }
3847
3848 impl Capability for PriorityFilterCap {
3849 fn id(&self) -> &str {
3850 self.id
3851 }
3852 fn name(&self) -> &str {
3853 self.id
3854 }
3855 fn description(&self) -> &str {
3856 "priority test"
3857 }
3858 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3859 Some(Arc::new(PriorityFilterProvider {
3860 search_term: self.search_term,
3861 priority: self.priority,
3862 }))
3863 }
3864 }
3865
3866 impl MessageFilterProvider for PriorityFilterProvider {
3867 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3868 query
3869 .filters
3870 .push(MessageFilter::Search(self.search_term.to_string()));
3871 }
3872 fn priority(&self) -> i32 {
3873 self.priority
3874 }
3875 }
3876
3877 let mut registry = CapabilityRegistry::new();
3878 registry.register(PriorityFilterCap {
3879 id: "gamma",
3880 search_term: "gamma",
3881 priority: 10,
3882 });
3883 registry.register(PriorityFilterCap {
3884 id: "alpha",
3885 search_term: "alpha",
3886 priority: 5,
3887 });
3888 registry.register(PriorityFilterCap {
3889 id: "beta",
3890 search_term: "beta",
3891 priority: 1,
3892 });
3893
3894 let configs = vec![
3895 AgentCapabilityConfig {
3896 capability_ref: CapabilityId::new("gamma"),
3897 config: serde_json::json!({}),
3898 },
3899 AgentCapabilityConfig {
3900 capability_ref: CapabilityId::new("alpha"),
3901 config: serde_json::json!({}),
3902 },
3903 AgentCapabilityConfig {
3904 capability_ref: CapabilityId::new("beta"),
3905 config: serde_json::json!({}),
3906 },
3907 ];
3908
3909 let collected = collect_message_filters_only(&configs, ®istry);
3910
3911 let session_id: SessionId = Uuid::now_v7().into();
3912 let mut query = MessageQuery::new(session_id);
3913 collected.apply_message_filters(&mut query);
3914
3915 assert_eq!(query.filters.len(), 3);
3917 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3918 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3919 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3920 }
3921
3922 #[test]
3923 fn test_collect_message_filters_only_post_load_invoked() {
3924 use crate::message::Message;
3925
3926 struct PostLoadCap;
3927 struct PostLoadProvider;
3928
3929 impl Capability for PostLoadCap {
3930 fn id(&self) -> &str {
3931 "post_load_test"
3932 }
3933 fn name(&self) -> &str {
3934 "PostLoad Test"
3935 }
3936 fn description(&self) -> &str {
3937 "test"
3938 }
3939 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3940 Some(Arc::new(PostLoadProvider))
3941 }
3942 }
3943
3944 impl MessageFilterProvider for PostLoadProvider {
3945 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
3946 fn priority(&self) -> i32 {
3947 0
3948 }
3949 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
3950 messages.reverse();
3952 }
3953 }
3954
3955 let mut registry = CapabilityRegistry::new();
3956 registry.register(PostLoadCap);
3957
3958 let configs = vec![AgentCapabilityConfig {
3959 capability_ref: CapabilityId::new("post_load_test"),
3960 config: serde_json::json!({}),
3961 }];
3962
3963 let collected = collect_message_filters_only(&configs, ®istry);
3964
3965 let mut messages = vec![Message::user("first"), Message::user("second")];
3966 collected.apply_post_load_filters(&mut messages);
3967
3968 assert_eq!(messages[0].text(), Some("second"));
3970 assert_eq!(messages[1].text(), Some("first"));
3971 }
3972
3973 #[test]
3974 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
3975 use crate::tool_types::ToolCall;
3976
3977 fn tool_heavy_messages() -> Vec<Message> {
3978 let mut messages = vec![Message::user("inspect files repeatedly")];
3979 for index in 0..9 {
3980 let call_id = format!("call_{index}");
3981 messages.push(Message::assistant_with_tools(
3982 "",
3983 vec![ToolCall {
3984 id: call_id.clone(),
3985 name: "read_file".to_string(),
3986 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
3987 }],
3988 ));
3989 messages.push(Message::tool_result(
3990 call_id,
3991 Some(serde_json::json!({
3992 "path": "/workspace/src/lib.rs",
3993 "content": format!("{}{}", "large file line\n".repeat(1000), index),
3994 "total_lines": 1000,
3995 "lines_shown": {"start": 1, "end": 1000},
3996 "truncated": false
3997 })),
3998 None,
3999 ));
4000 }
4001 messages
4002 }
4003
4004 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4005 messages[2]
4006 .tool_result_content()
4007 .and_then(|result| result.result.as_ref())
4008 .and_then(|result| result.get("masked"))
4009 .and_then(|masked| masked.as_bool())
4010 .unwrap_or(false)
4011 }
4012
4013 let mut registry = CapabilityRegistry::new();
4014 registry.register(CompactionCapability);
4015 let context = ModelViewContext {
4016 session_id: SessionId::new(),
4017 prior_usage: None,
4018 };
4019
4020 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4021 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4022 assert!(!first_tool_result_is_masked(&unmasked));
4023
4024 let compaction = collect_model_view_providers(
4025 &[AgentCapabilityConfig {
4026 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4027 config: serde_json::json!({}),
4028 }],
4029 ®istry,
4030 None,
4031 );
4032 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4033 assert!(first_tool_result_is_masked(&masked));
4034 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4035 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4036 }
4037
4038 struct DelegatingFilterCap {
4041 id: &'static str,
4042 inner: std::sync::Arc<InnerFilterCap>,
4043 }
4044 struct InnerFilterCap;
4045
4046 impl Capability for InnerFilterCap {
4047 fn id(&self) -> &str {
4048 "inner_filter"
4049 }
4050 fn name(&self) -> &str {
4051 "Inner Filter"
4052 }
4053 fn description(&self) -> &str {
4054 "inner"
4055 }
4056 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4057 Some(std::sync::Arc::new(SentinelFilter))
4058 }
4059 }
4060 struct SentinelFilter;
4061 impl MessageFilterProvider for SentinelFilter {
4062 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4063 }
4064 impl Capability for DelegatingFilterCap {
4065 fn id(&self) -> &str {
4066 self.id
4067 }
4068 fn name(&self) -> &str {
4069 "Delegating Filter"
4070 }
4071 fn description(&self) -> &str {
4072 "delegating"
4073 }
4074 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4075 None }
4077 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4078 Some(&*self.inner)
4079 }
4080 }
4081
4082 #[test]
4083 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4084 let inner = std::sync::Arc::new(InnerFilterCap);
4085 let outer = DelegatingFilterCap {
4086 id: "delegating_filter",
4087 inner: inner.clone(),
4088 };
4089
4090 let mut registry = CapabilityRegistry::new();
4091 registry.register(outer);
4092
4093 let configs = vec![AgentCapabilityConfig {
4094 capability_ref: CapabilityId::new("delegating_filter"),
4095 config: serde_json::json!({}),
4096 }];
4097
4098 let collected = collect_message_filters_only(&configs, ®istry);
4101 assert_eq!(
4102 collected.message_filter_providers.len(),
4103 1,
4104 "provider from resolved inner capability must be collected"
4105 );
4106 }
4107
4108 struct DelegatingMvpCap {
4109 id: &'static str,
4110 inner: std::sync::Arc<InnerMvpCap>,
4111 }
4112 struct InnerMvpCap;
4113
4114 impl Capability for InnerMvpCap {
4115 fn id(&self) -> &str {
4116 "inner_mvp"
4117 }
4118 fn name(&self) -> &str {
4119 "Inner MVP"
4120 }
4121 fn description(&self) -> &str {
4122 "inner"
4123 }
4124 fn model_view_provider(
4125 &self,
4126 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4127 struct NoopMvp;
4129 impl crate::capabilities::ModelViewProvider for NoopMvp {
4130 fn apply_model_view(
4131 &self,
4132 messages: Vec<Message>,
4133 _config: &serde_json::Value,
4134 _context: &ModelViewContext<'_>,
4135 ) -> Vec<Message> {
4136 messages
4137 }
4138 }
4139 Some(std::sync::Arc::new(NoopMvp))
4140 }
4141 }
4142 impl Capability for DelegatingMvpCap {
4143 fn id(&self) -> &str {
4144 self.id
4145 }
4146 fn name(&self) -> &str {
4147 "Delegating MVP"
4148 }
4149 fn description(&self) -> &str {
4150 "delegating"
4151 }
4152 fn model_view_provider(
4153 &self,
4154 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4155 None }
4157 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4158 Some(&*self.inner)
4159 }
4160 }
4161
4162 #[test]
4163 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4164 let inner = std::sync::Arc::new(InnerMvpCap);
4165 let outer = DelegatingMvpCap {
4166 id: "delegating_mvp",
4167 inner: inner.clone(),
4168 };
4169
4170 let mut registry = CapabilityRegistry::new();
4171 registry.register(outer);
4172
4173 let configs = vec![AgentCapabilityConfig {
4174 capability_ref: CapabilityId::new("delegating_mvp"),
4175 config: serde_json::json!({}),
4176 }];
4177
4178 let collected = collect_model_view_providers(&configs, ®istry, None);
4181 assert_eq!(
4182 collected.model_view_providers.len(),
4183 1,
4184 "provider from resolved inner capability must be collected"
4185 );
4186 }
4187
4188 #[tokio::test]
4198 async fn test_bashkit_shell_capability_produces_bash_tool() {
4199 let registry = CapabilityRegistry::with_builtins();
4200 let collected =
4201 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4202
4203 let tool_names: Vec<&str> = collected
4204 .tool_definitions
4205 .iter()
4206 .map(|t| t.name())
4207 .collect();
4208 assert!(
4209 tool_names.contains(&"bash"),
4210 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4211 tool_names
4212 );
4213 assert!(
4214 !collected.tools.is_empty(),
4215 "bashkit_shell must provide tool implementations"
4216 );
4217 }
4218
4219 #[tokio::test]
4220 async fn test_generic_harness_capability_set_produces_bash_tool() {
4221 let generic_harness_caps = vec![
4224 "session_file_system".to_string(),
4225 "bashkit_shell".to_string(),
4226 "web_fetch".to_string(),
4227 "session_storage".to_string(),
4228 "session".to_string(),
4229 "agent_instructions".to_string(),
4230 "skills".to_string(),
4231 "infinity_context".to_string(),
4232 "auto_tool_search".to_string(),
4233 ];
4234
4235 let registry = CapabilityRegistry::with_builtins();
4236 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4237
4238 let tool_names: Vec<&str> = collected
4239 .tool_definitions
4240 .iter()
4241 .map(|t| t.name())
4242 .collect();
4243 assert!(
4244 tool_names.contains(&"bash"),
4245 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4246 tool_names
4247 );
4248 }
4249
4250 #[tokio::test]
4251 async fn test_collect_capabilities_tool_count_matches_definitions() {
4252 let registry = CapabilityRegistry::with_builtins();
4255 let collected =
4256 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4257
4258 assert_eq!(
4259 collected.tools.len(),
4260 collected.tool_definitions.len(),
4261 "tool implementations ({}) must match tool definitions ({})",
4262 collected.tools.len(),
4263 collected.tool_definitions.len(),
4264 );
4265 }
4266
4267 #[tokio::test]
4271 async fn test_collect_capabilities_resolves_dependencies() {
4272 let registry = CapabilityRegistry::with_builtins();
4275 let collected =
4276 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4277
4278 assert!(
4280 collected
4281 .applied_ids
4282 .iter()
4283 .any(|id| id == "session_file_system"),
4284 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4285 collected.applied_ids
4286 );
4287
4288 let tool_names: Vec<&str> = collected
4289 .tool_definitions
4290 .iter()
4291 .map(|t| t.name())
4292 .collect();
4293
4294 assert!(
4296 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4297 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
4298 tool_names
4299 );
4300
4301 assert_eq!(
4303 collected.tools.len(),
4304 collected.tool_definitions.len(),
4305 "dependency-added tools must have implementations, not just definitions"
4306 );
4307 }
4308
4309 #[test]
4310 fn test_defaults_do_not_include_bash() {
4311 let registry = crate::ToolRegistry::with_defaults();
4314 assert!(
4315 !registry.has("bash"),
4316 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
4317 );
4318 }
4319
4320 #[tokio::test]
4327 async fn test_background_execution_auto_activates_with_bashkit_shell() {
4328 let registry = CapabilityRegistry::with_builtins();
4329 let collected =
4330 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4331
4332 let tool_names: Vec<&str> = collected
4333 .tool_definitions
4334 .iter()
4335 .map(|t| t.name())
4336 .collect();
4337 assert!(
4338 tool_names.contains(&"spawn_background"),
4339 "spawn_background must be auto-activated when bashkit_shell (a \
4340 background-capable tool) is in the agent's capability set; got: {:?}",
4341 tool_names
4342 );
4343 assert!(
4344 collected
4345 .applied_ids
4346 .iter()
4347 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4348 "background_execution must be in applied_ids when auto-activated; \
4349 got: {:?}",
4350 collected.applied_ids
4351 );
4352
4353 assert!(
4355 collected
4356 .tools
4357 .iter()
4358 .any(|t| t.name() == "spawn_background"),
4359 "spawn_background tool implementation must be present alongside the \
4360 definition (lockstep contract)"
4361 );
4362 }
4363
4364 #[tokio::test]
4367 async fn test_background_execution_does_not_auto_activate_without_hint() {
4368 let registry = CapabilityRegistry::with_builtins();
4369 let collected =
4371 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4372
4373 let tool_names: Vec<&str> = collected
4374 .tool_definitions
4375 .iter()
4376 .map(|t| t.name())
4377 .collect();
4378 assert!(
4379 !tool_names.contains(&"spawn_background"),
4380 "spawn_background must NOT be activated without a background-capable \
4381 tool; got: {:?}",
4382 tool_names
4383 );
4384 assert!(
4385 !collected
4386 .applied_ids
4387 .iter()
4388 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4389 "background_execution must not appear in applied_ids when no \
4390 background-capable tool is present; got: {:?}",
4391 collected.applied_ids
4392 );
4393 }
4394
4395 #[tokio::test]
4399 async fn test_background_execution_explicit_selection_is_idempotent() {
4400 let registry = CapabilityRegistry::with_builtins();
4401 let collected = collect_capabilities(
4402 &[
4403 "bashkit_shell".to_string(),
4404 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
4405 ],
4406 ®istry,
4407 &test_ctx(),
4408 )
4409 .await;
4410
4411 let spawn_background_count = collected
4412 .tool_definitions
4413 .iter()
4414 .filter(|t| t.name() == "spawn_background")
4415 .count();
4416 assert_eq!(
4417 spawn_background_count, 1,
4418 "spawn_background must appear exactly once even when \
4419 background_execution is selected explicitly alongside a \
4420 background-capable tool"
4421 );
4422 let applied_count = collected
4423 .applied_ids
4424 .iter()
4425 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
4426 .count();
4427 assert_eq!(
4428 applied_count, 1,
4429 "background_execution must appear exactly once in applied_ids"
4430 );
4431 }
4432
4433 #[test]
4438 fn test_defaults_do_not_include_spawn_background() {
4439 let registry = crate::ToolRegistry::with_defaults();
4440 assert!(
4441 !registry.has("spawn_background"),
4442 "with_defaults() must not include 'spawn_background' — it comes \
4443 from the background_execution capability (EVE-501)"
4444 );
4445 }
4446
4447 #[test]
4452 fn test_capability_features_default_empty() {
4453 let registry = CapabilityRegistry::with_builtins();
4454
4455 let noop = registry.get("noop").unwrap();
4457 assert!(noop.features().is_empty());
4458
4459 let current_time = registry.get("current_time").unwrap();
4460 assert!(current_time.features().is_empty());
4461 }
4462
4463 #[test]
4464 fn test_file_system_capability_features() {
4465 let registry = CapabilityRegistry::with_builtins();
4466
4467 let fs = registry.get("session_file_system").unwrap();
4468 assert_eq!(fs.features(), vec!["file_system"]);
4469 }
4470
4471 #[test]
4472 fn test_bashkit_shell_capability_features() {
4473 let registry = CapabilityRegistry::with_builtins();
4474
4475 let bash = registry.get("bashkit_shell").unwrap();
4476 assert_eq!(bash.features(), vec!["file_system"]);
4477 }
4478
4479 #[test]
4480 fn test_alias_resolves_to_canonical_capability() {
4481 let registry = CapabilityRegistry::with_builtins();
4482
4483 let via_alias = registry.get("virtual_bash").unwrap();
4485 assert_eq!(via_alias.id(), "bashkit_shell");
4486 assert!(registry.has("virtual_bash"));
4487 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
4488 assert_eq!(
4489 registry.canonical_id("bashkit_shell"),
4490 Some("bashkit_shell")
4491 );
4492 assert_eq!(registry.canonical_id("nonexistent"), None);
4493 }
4494
4495 #[test]
4496 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
4497 let registry = CapabilityRegistry::with_builtins();
4498
4499 let resolved = resolve_dependencies(
4502 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
4503 ®istry,
4504 )
4505 .unwrap();
4506 let bash_ids: Vec<_> = resolved
4507 .resolved_ids
4508 .iter()
4509 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
4510 .collect();
4511 assert_eq!(bash_ids, vec!["bashkit_shell"]);
4512 assert!(
4514 !resolved
4515 .added_as_dependencies
4516 .contains(&"bashkit_shell".to_string())
4517 );
4518 }
4519
4520 #[test]
4521 fn test_alias_preserves_explicit_config_in_resolution() {
4522 let registry = CapabilityRegistry::with_builtins();
4523
4524 let configs = vec![AgentCapabilityConfig::with_config(
4525 "virtual_bash".to_string(),
4526 serde_json::json!({"key": "value"}),
4527 )];
4528 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
4529 let bash = resolved
4530 .iter()
4531 .find(|c| c.capability_id() == "bashkit_shell")
4532 .expect("alias must resolve to canonical bashkit_shell config");
4533 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
4534 }
4535
4536 #[test]
4537 fn test_unregister_by_alias_removes_capability_and_aliases() {
4538 let mut registry = CapabilityRegistry::with_builtins();
4539
4540 assert!(registry.unregister("virtual_bash").is_some());
4541 assert!(!registry.has("bashkit_shell"));
4542 assert!(!registry.has("virtual_bash"));
4543 }
4544
4545 #[test]
4546 fn test_session_storage_capability_features() {
4547 let registry = CapabilityRegistry::with_builtins();
4548
4549 let storage = registry.get("session_storage").unwrap();
4550 let features = storage.features();
4551 assert!(features.contains(&"secrets"));
4552 assert!(features.contains(&"key_value"));
4553 }
4554
4555 #[test]
4556 fn test_session_schedule_capability_features() {
4557 let registry = CapabilityRegistry::with_builtins();
4558
4559 let schedule = registry.get("session_schedule").unwrap();
4560 assert_eq!(schedule.features(), vec!["schedules"]);
4561 }
4562
4563 #[test]
4564 fn test_session_sql_database_capability_features() {
4565 let registry = CapabilityRegistry::with_builtins();
4566
4567 let sql = registry.get("session_sql_database").unwrap();
4568 assert_eq!(sql.features(), vec!["sql_database"]);
4569 }
4570
4571 #[test]
4572 fn test_sample_data_capability_features() {
4573 let registry = CapabilityRegistry::with_builtins();
4574
4575 let sample = registry.get("sample_data").unwrap();
4576 assert_eq!(sample.features(), vec!["file_system"]);
4577 }
4578
4579 #[test]
4580 fn test_compute_features_empty() {
4581 let registry = CapabilityRegistry::with_builtins();
4582
4583 let features = compute_features(&[], ®istry);
4584 assert!(features.is_empty());
4585 }
4586
4587 #[test]
4588 fn test_compute_features_single_capability() {
4589 let registry = CapabilityRegistry::with_builtins();
4590
4591 let features = compute_features(&["session_schedule".to_string()], ®istry);
4592 assert_eq!(features, vec!["schedules"]);
4593 }
4594
4595 #[test]
4596 fn test_compute_features_multiple_capabilities() {
4597 let registry = CapabilityRegistry::with_builtins();
4598
4599 let features = compute_features(
4600 &[
4601 "session_file_system".to_string(),
4602 "session_storage".to_string(),
4603 "session_schedule".to_string(),
4604 ],
4605 ®istry,
4606 );
4607 assert!(features.contains(&"file_system".to_string()));
4608 assert!(features.contains(&"secrets".to_string()));
4609 assert!(features.contains(&"key_value".to_string()));
4610 assert!(features.contains(&"schedules".to_string()));
4611 }
4612
4613 #[test]
4614 fn test_compute_features_deduplicates() {
4615 let registry = CapabilityRegistry::with_builtins();
4616
4617 let features = compute_features(
4619 &[
4620 "session_file_system".to_string(),
4621 "bashkit_shell".to_string(),
4622 ],
4623 ®istry,
4624 );
4625 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
4626 assert_eq!(file_system_count, 1, "file_system should appear only once");
4627 }
4628
4629 #[test]
4630 fn test_compute_features_includes_dependency_features() {
4631 let registry = CapabilityRegistry::with_builtins();
4632
4633 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
4635 assert!(features.contains(&"file_system".to_string()));
4636 }
4637
4638 #[test]
4639 fn test_compute_features_generic_harness_set() {
4640 let registry = CapabilityRegistry::with_builtins();
4641
4642 let features = compute_features(
4644 &[
4645 "session_file_system".to_string(),
4646 "bashkit_shell".to_string(),
4647 "session_storage".to_string(),
4648 "session".to_string(),
4649 "session_schedule".to_string(),
4650 ],
4651 ®istry,
4652 );
4653 assert!(features.contains(&"file_system".to_string()));
4654 assert!(features.contains(&"secrets".to_string()));
4655 assert!(features.contains(&"key_value".to_string()));
4656 assert!(features.contains(&"schedules".to_string()));
4657 }
4658
4659 #[test]
4660 fn test_compute_features_unknown_capability_ignored() {
4661 let registry = CapabilityRegistry::with_builtins();
4662
4663 let features = compute_features(
4664 &["unknown_cap".to_string(), "session_schedule".to_string()],
4665 ®istry,
4666 );
4667 assert_eq!(features, vec!["schedules"]);
4668 }
4669
4670 #[test]
4671 fn test_risk_level_ordering() {
4672 assert!(RiskLevel::Low < RiskLevel::Medium);
4673 assert!(RiskLevel::Medium < RiskLevel::High);
4674 }
4675
4676 #[test]
4677 fn test_risk_level_serde_roundtrip() {
4678 let high = RiskLevel::High;
4679 let json = serde_json::to_string(&high).unwrap();
4680 assert_eq!(json, "\"high\"");
4681 let back: RiskLevel = serde_json::from_str(&json).unwrap();
4682 assert_eq!(back, RiskLevel::High);
4683 }
4684
4685 #[test]
4686 fn test_capability_risk_levels() {
4687 let registry = CapabilityRegistry::with_builtins();
4688
4689 let bash = registry.get("bashkit_shell").unwrap();
4691 assert_eq!(bash.risk_level(), RiskLevel::High);
4692
4693 let fetch = registry.get("web_fetch").unwrap();
4695 assert_eq!(fetch.risk_level(), RiskLevel::High);
4696
4697 let noop = registry.get("noop").unwrap();
4699 assert_eq!(noop.risk_level(), RiskLevel::Low);
4700 }
4701
4702 #[tokio::test]
4707 async fn test_apply_capabilities_openai_tool_search() {
4708 let registry = CapabilityRegistry::with_builtins();
4709 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4710
4711 let applied = apply_capabilities(
4712 base_runtime_agent.clone(),
4713 &["openai_tool_search".to_string()],
4714 ®istry,
4715 &test_ctx(),
4716 )
4717 .await;
4718
4719 assert_eq!(
4721 applied.runtime_agent.system_prompt,
4722 base_runtime_agent.system_prompt
4723 );
4724 assert!(applied.tool_registry.is_empty());
4725 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
4726
4727 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4729 assert!(ts.enabled);
4730 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4731 }
4732
4733 #[tokio::test]
4734 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
4735 let registry = CapabilityRegistry::with_builtins();
4736 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4737
4738 let applied = apply_capabilities(
4739 base_runtime_agent,
4740 &[
4741 "current_time".to_string(),
4742 "openai_tool_search".to_string(),
4743 "test_math".to_string(),
4744 ],
4745 ®istry,
4746 &test_ctx(),
4747 )
4748 .await;
4749
4750 assert!(applied.tool_registry.has("get_current_time"));
4752 assert!(applied.tool_registry.has("add"));
4753 assert!(applied.tool_registry.has("subtract"));
4754 assert!(applied.tool_registry.has("multiply"));
4755 assert!(applied.tool_registry.has("divide"));
4756
4757 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4759 assert!(ts.enabled);
4760 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4761 }
4762
4763 #[tokio::test]
4764 async fn test_collect_capabilities_tool_search_custom_threshold() {
4765 let registry = CapabilityRegistry::with_builtins();
4766
4767 let configs = vec![AgentCapabilityConfig {
4768 capability_ref: CapabilityId::new("openai_tool_search"),
4769 config: serde_json::json!({"threshold": 5}),
4770 }];
4771
4772 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4773
4774 let ts = collected.tool_search.as_ref().unwrap();
4775 assert!(ts.enabled);
4776 assert_eq!(ts.threshold, 5);
4777 }
4778
4779 #[tokio::test]
4780 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
4781 let registry = CapabilityRegistry::with_builtins();
4782
4783 let configs = vec![
4784 AgentCapabilityConfig {
4785 capability_ref: CapabilityId::new("auto_tool_search"),
4786 config: serde_json::json!({"threshold": 2}),
4787 },
4788 AgentCapabilityConfig {
4789 capability_ref: CapabilityId::new("test_math"),
4790 config: serde_json::json!({}),
4791 },
4792 ];
4793
4794 let ctx = test_ctx().with_model("claude-3-5-haiku");
4798 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4799
4800 assert!(
4801 collected.tool_search.is_none(),
4802 "auto_tool_search must not set a hosted config on a non-native model"
4803 );
4804 assert!(
4805 collected
4806 .tools
4807 .iter()
4808 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4809 "auto_tool_search must contribute the client-side tool_search tool"
4810 );
4811 assert!(
4812 !collected.tool_definition_hooks.is_empty(),
4813 "auto_tool_search must contribute a client-side deferral hook"
4814 );
4815
4816 let mut transformed = collected.tool_definitions.clone();
4817 for hook in &collected.tool_definition_hooks {
4818 transformed = hook.transform(transformed);
4819 }
4820 let add_tool = transformed
4821 .iter()
4822 .find(|tool| tool.name() == "add")
4823 .expect("test_math contributes add");
4824 assert!(
4825 add_tool.parameters().get("properties").is_none(),
4826 "generic auto_tool_search must honor the configured threshold"
4827 );
4828 }
4829
4830 #[tokio::test]
4831 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
4832 let registry = CapabilityRegistry::with_builtins();
4833
4834 let configs = vec![AgentCapabilityConfig {
4835 capability_ref: CapabilityId::new("auto_tool_search"),
4836 config: serde_json::json!({"threshold": 7}),
4837 }];
4838
4839 let ctx = test_ctx().with_model("gpt-5.4");
4842 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4843
4844 let ts = collected
4845 .tool_search
4846 .as_ref()
4847 .expect("auto_tool_search must set a hosted config on a native model");
4848 assert!(ts.enabled);
4849 assert_eq!(ts.threshold, 7);
4850 assert!(
4851 !collected
4852 .tools
4853 .iter()
4854 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4855 "hosted mechanism must not contribute the client-side tool_search tool"
4856 );
4857 assert!(
4858 collected.tool_definition_hooks.is_empty(),
4859 "hosted mechanism must not contribute a client-side deferral hook"
4860 );
4861 }
4862
4863 #[tokio::test]
4864 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
4865 let registry = CapabilityRegistry::with_builtins();
4866
4867 let configs = vec![AgentCapabilityConfig {
4868 capability_ref: CapabilityId::new("auto_tool_search"),
4869 config: serde_json::json!({"threshold": 9}),
4870 }];
4871
4872 let ctx = test_ctx().with_model("claude-opus-4-8");
4875 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4876
4877 let ts = collected
4878 .tool_search
4879 .as_ref()
4880 .expect("auto_tool_search must set a hosted config on a native Claude model");
4881 assert!(ts.enabled);
4882 assert_eq!(ts.threshold, 9);
4883 assert!(
4884 !collected
4885 .tools
4886 .iter()
4887 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4888 "hosted mechanism must not contribute the client-side tool_search tool"
4889 );
4890 assert!(
4891 collected.tool_definition_hooks.is_empty(),
4892 "hosted mechanism must not contribute a client-side deferral hook"
4893 );
4894 }
4895
4896 #[tokio::test]
4897 async fn test_collect_capabilities_no_tool_search_without_capability() {
4898 let registry = CapabilityRegistry::with_builtins();
4899
4900 let configs = vec![AgentCapabilityConfig {
4901 capability_ref: CapabilityId::new("current_time"),
4902 config: serde_json::json!({}),
4903 }];
4904
4905 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4906
4907 assert!(collected.tool_search.is_none());
4908 }
4909
4910 #[tokio::test]
4911 async fn test_collect_capabilities_tool_search_category_propagation() {
4912 let registry = CapabilityRegistry::with_builtins();
4913
4914 let configs = vec![
4916 AgentCapabilityConfig {
4917 capability_ref: CapabilityId::new("test_math"),
4918 config: serde_json::json!({}),
4919 },
4920 AgentCapabilityConfig {
4921 capability_ref: CapabilityId::new("openai_tool_search"),
4922 config: serde_json::json!({}),
4923 },
4924 ];
4925
4926 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4927
4928 assert!(collected.tool_search.is_some());
4930
4931 for tool_def in &collected.tool_definitions {
4933 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
4935 assert!(
4936 tool_def.category().is_some(),
4937 "Tool {} should have a category from its capability",
4938 tool_def.name()
4939 );
4940 }
4941 }
4942 }
4943
4944 #[tokio::test]
4945 async fn test_apply_capabilities_prompt_caching() {
4946 let registry = CapabilityRegistry::with_builtins();
4947 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4948
4949 let applied = apply_capabilities(
4950 base_runtime_agent.clone(),
4951 &["prompt_caching".to_string()],
4952 ®istry,
4953 &test_ctx(),
4954 )
4955 .await;
4956
4957 assert_eq!(
4958 applied.runtime_agent.system_prompt,
4959 base_runtime_agent.system_prompt
4960 );
4961 assert!(applied.tool_registry.is_empty());
4962 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
4963
4964 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
4965 assert!(prompt_cache.enabled);
4966 assert_eq!(
4967 prompt_cache.strategy,
4968 crate::driver_registry::PromptCacheStrategy::Auto
4969 );
4970 assert!(prompt_cache.gemini_cached_content.is_none());
4971 }
4972
4973 #[tokio::test]
4974 async fn test_apply_capabilities_openrouter_server_tools() {
4975 let registry = CapabilityRegistry::with_builtins();
4976 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4977
4978 let configs = vec![AgentCapabilityConfig {
4979 capability_ref: CapabilityId::new("openrouter_server_tools"),
4980 config: serde_json::json!({
4981 "tools": ["web_search", "datetime"],
4982 "web_search_max_results": 4,
4983 }),
4984 }];
4985
4986 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4987 let routing = collected
4988 .openrouter_routing
4989 .as_ref()
4990 .expect("server tools produce routing config");
4991 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
4992 assert_eq!(
4993 kinds,
4994 vec![
4995 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
4996 crate::driver_registry::OpenRouterServerToolKind::Datetime,
4997 ]
4998 );
4999
5000 let applied = apply_capabilities(
5003 base_runtime_agent,
5004 &["openrouter_server_tools".to_string()],
5005 ®istry,
5006 &test_ctx(),
5007 )
5008 .await;
5009 assert!(applied.tool_registry.is_empty());
5010 assert!(applied.runtime_agent.openrouter_routing.is_none());
5011 }
5012
5013 #[tokio::test]
5014 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
5015 let registry = CapabilityRegistry::with_builtins();
5016
5017 let configs = vec![AgentCapabilityConfig {
5018 capability_ref: CapabilityId::new("prompt_caching"),
5019 config: serde_json::json!({"strategy": "auto"}),
5020 }];
5021
5022 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5023
5024 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5025 assert!(prompt_cache.enabled);
5026 assert_eq!(
5027 prompt_cache.strategy,
5028 crate::driver_registry::PromptCacheStrategy::Auto
5029 );
5030 assert!(prompt_cache.gemini_cached_content.is_none());
5031 }
5032
5033 #[tokio::test]
5034 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
5035 let registry = CapabilityRegistry::with_builtins();
5036
5037 let configs = vec![AgentCapabilityConfig {
5038 capability_ref: CapabilityId::new("prompt_caching"),
5039 config: serde_json::json!({
5040 "strategy": "auto",
5041 "gemini_cached_content": "cachedContents/demo-cache"
5042 }),
5043 }];
5044
5045 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5046
5047 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5048 assert_eq!(
5049 prompt_cache.gemini_cached_content.as_deref(),
5050 Some("cachedContents/demo-cache")
5051 );
5052 }
5053
5054 struct SkillContributingCapability;
5059
5060 impl Capability for SkillContributingCapability {
5061 fn id(&self) -> &str {
5062 "contributes_skills"
5063 }
5064 fn name(&self) -> &str {
5065 "Contributes Skills"
5066 }
5067 fn description(&self) -> &str {
5068 "Test capability that contributes skills."
5069 }
5070 fn contribute_skills(&self) -> Vec<SkillContribution> {
5071 vec![
5072 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
5073 .with_files(vec![(
5074 "scripts/a.sh".to_string(),
5075 "#!/bin/sh\necho a\n".to_string(),
5076 )]),
5077 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
5078 .with_user_invocable(false),
5079 ]
5080 }
5081 }
5082
5083 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
5084 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
5085 MountSource::InlineFile { content, .. } => content.as_str(),
5086 _ => panic!("Expected InlineFile for SKILL.md"),
5087 }
5088 }
5089
5090 #[tokio::test]
5091 async fn test_contribute_skills_normalized_to_mounts() {
5092 let mut registry = CapabilityRegistry::new();
5093 registry.register(SkillContributingCapability);
5094
5095 let configs = vec![AgentCapabilityConfig {
5096 capability_ref: CapabilityId::new("contributes_skills"),
5097 config: serde_json::json!({}),
5098 }];
5099
5100 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5101
5102 let skill_mounts: Vec<_> = collected
5103 .mounts
5104 .iter()
5105 .filter(|m| m.path.starts_with("/.agents/skills/"))
5106 .collect();
5107 assert_eq!(skill_mounts.len(), 2);
5108
5109 for m in &skill_mounts {
5112 assert!(m.is_readonly());
5113 assert_eq!(m.capability_id, "contributes_skills");
5114 }
5115
5116 let alpha = skill_mounts
5117 .iter()
5118 .find(|m| m.path == "/.agents/skills/alpha-skill")
5119 .expect("alpha-skill mount missing");
5120 match &alpha.source {
5121 MountSource::InlineDirectory { entries } => {
5122 assert!(entries.contains_key("SKILL.md"));
5123 assert!(entries.contains_key("scripts/a.sh"));
5124 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5125 assert_eq!(parsed.name, "alpha-skill");
5126 assert!(parsed.user_invocable);
5127 }
5128 _ => panic!("Expected InlineDirectory"),
5129 }
5130
5131 let beta = skill_mounts
5132 .iter()
5133 .find(|m| m.path == "/.agents/skills/beta-skill")
5134 .expect("beta-skill mount missing");
5135 match &beta.source {
5136 MountSource::InlineDirectory { entries } => {
5137 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5138 assert!(!parsed.user_invocable);
5139 }
5140 _ => panic!("Expected InlineDirectory"),
5141 }
5142 }
5143
5144 #[tokio::test]
5145 async fn test_contribute_skills_default_empty() {
5146 let mut registry = CapabilityRegistry::new();
5149 registry.register(FilterTestCapability { priority: 0 });
5150
5151 let configs = vec![AgentCapabilityConfig {
5152 capability_ref: CapabilityId::new("filter_test"),
5153 config: serde_json::json!({}),
5154 }];
5155
5156 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5157 assert!(
5158 collected
5159 .mounts
5160 .iter()
5161 .all(|m| !m.path.starts_with("/.agents/skills/"))
5162 );
5163 }
5164
5165 struct LocalizedCapability;
5166
5167 impl Capability for LocalizedCapability {
5168 fn id(&self) -> &str {
5169 "localized"
5170 }
5171 fn name(&self) -> &str {
5172 "Localized"
5173 }
5174 fn description(&self) -> &str {
5175 "English description"
5176 }
5177 fn localizations(&self) -> Vec<CapabilityLocalization> {
5178 vec![
5179 CapabilityLocalization {
5180 locale: "en",
5181 name: None,
5182 description: None,
5183 config_description: Some("Controls things."),
5184 config_overlay: None,
5185 },
5186 CapabilityLocalization {
5187 locale: "uk",
5188 name: Some("Локалізована"),
5189 description: Some("Український опис"),
5190 config_description: Some("Керує налаштуваннями."),
5191 config_overlay: None,
5192 },
5193 ]
5194 }
5195 }
5196
5197 #[test]
5198 fn localized_name_falls_back_exact_language_then_base() {
5199 let cap = LocalizedCapability;
5200 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
5202 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
5203 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
5205 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
5207 assert_eq!(cap.localized_name(None), "Localized");
5208 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
5209 assert_eq!(cap.localized_description(Some("de")), "English description");
5210 }
5211
5212 #[test]
5213 fn describe_schema_resolves_config_description_per_locale() {
5214 let cap = LocalizedCapability;
5215 assert_eq!(
5216 cap.describe_schema(Some("uk-UA")).as_deref(),
5217 Some("Керує налаштуваннями.")
5218 );
5219 assert_eq!(
5221 cap.describe_schema(Some("pl")).as_deref(),
5222 Some("Controls things.")
5223 );
5224 assert_eq!(
5225 cap.describe_schema(None).as_deref(),
5226 Some("Controls things.")
5227 );
5228 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
5230 }
5231}