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
81#[cfg(feature = "a2a")]
86mod a2a_delegation;
87#[cfg(feature = "ui-capabilities")]
88mod a2ui;
89mod agent_handoff;
90mod agent_instructions;
91pub mod attach_skill;
92mod auto_tool_search;
93mod background_execution;
94mod bashkit_shell;
95mod btw;
96mod budgeting;
97mod claude_tool_search;
98pub mod compaction;
99mod current_time;
100mod data_knowledge;
101mod declarative;
102mod error_disclosure;
103mod fake_aws;
104mod fake_crm;
105mod fake_financial;
106mod fake_warehouse;
107mod file_system;
108mod guardrails;
109mod human_intent;
110mod infinity_context;
111mod knowledge_base;
112mod knowledge_index;
113mod loop_detection;
114mod lua;
115mod lua_code_mode;
116pub mod mcp;
117mod memory;
118mod message_metadata;
119mod model_scout;
120mod monitors;
121mod noop;
122mod openai_tool_search;
123mod openrouter_server_tools;
124mod openrouter_workspace;
125#[cfg(feature = "ui-capabilities")]
126mod openui;
127mod parallel_tool_calls;
128mod platform_management;
129mod prompt_caching;
130mod prompt_canary_guardrail;
131mod research;
132mod sample_data;
133mod self_budget;
134mod session;
135mod session_sandbox;
136mod session_schedule;
137mod session_sql_database;
138mod session_storage;
139mod session_tasks;
140mod skills;
141mod skills_scoped;
142mod stateless_todo_list;
143mod subagents;
144mod system_commands;
145mod test_math;
146mod test_weather;
147mod tool_call_repair;
148mod tool_output_distillation;
149mod tool_output_persistence;
150mod tool_search;
151pub mod user_hooks;
152mod util;
153#[cfg(feature = "web-fetch")]
154mod web_fetch;
155
156pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
161pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
165#[cfg(feature = "a2a")]
166pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
167#[cfg(feature = "ui-capabilities")]
168pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
169pub use agent_handoff::{
170 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
171 MessageAgentHandoffTool, StartAgentHandoffTool,
172};
173pub use agent_instructions::{
174 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
175 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
176 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
177};
178pub use attach_skill::{
179 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
180 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
181 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
182};
183pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
184pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
185pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
186pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
187pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
188pub use compaction::{
189 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
190 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
191 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
192 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
193 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
194 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
195 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
196};
197pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
198pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
199pub use declarative::{
200 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
201 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
202 declarative_capability_info, hydrate_declarative_capability_config,
203 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
204 plugin_capability_info, validate_declarative_capability_definition,
205};
206pub use error_disclosure::{
207 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
208};
209pub use fake_aws::{
210 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
211 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
212 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
213 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
214};
215pub use fake_crm::{
216 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
217 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
218 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
219};
220pub use fake_financial::{
221 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
222 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
223 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
224 FinanceListTransactionsTool,
225};
226pub use fake_warehouse::{
227 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
228 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
229 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
230 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
231};
232pub use file_system::{
233 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
234 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
235};
236pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
237pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
238pub use infinity_context::{
239 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
240};
241pub use knowledge_base::{
242 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
243 validate_knowledge_base_config,
244};
245pub use knowledge_index::{
246 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
247 validate_knowledge_index_config,
248};
249pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
250pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
251pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
252pub use mcp::{
253 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
254 parse_mcp_capability_id,
255};
256pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
257pub use message_metadata::{
258 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
259 MessageMetadataField, render_annotation,
260};
261pub use model_scout::{
262 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
263 RouterUpdateProposal, compute_score, rank_results,
264};
265pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
266pub use openai_tool_search::{
267 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
268 model_supports_native_tool_search,
269};
270pub use openrouter_server_tools::{
271 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
272};
273pub use openrouter_workspace::{
274 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
275 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
276 detect_policy_drift,
277};
278#[cfg(feature = "ui-capabilities")]
279pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
280pub use parallel_tool_calls::{
281 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
282 parallel_tool_calls_from_config,
283};
284pub use platform_management::{
285 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
286 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
287 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
288};
289pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
290pub use prompt_canary_guardrail::{
291 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
292 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
293 REASON_CODE_SYSTEM_PROMPT_LEAK,
294};
295pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
296pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
297pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
298pub use session::{
299 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
300};
301pub use session_sandbox::{
302 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
303 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
304};
305pub use session_schedule::{
306 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
307 SessionScheduleCapability,
308};
309pub use session_sql_database::{
310 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
311 SqlSchemaTool,
312};
313pub use session_storage::{
314 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
315 is_internal_session_kv_key,
316};
317pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
318pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
319pub use skills_scoped::{
320 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
321};
322pub use stateless_todo_list::{
323 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
324};
325pub use subagents::{SUBAGENTS_CAPABILITY_ID, SubagentCapability};
326pub use bashkit_shell::{
328 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
329};
330pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
331pub use test_math::{
332 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
333};
334pub use test_weather::{
335 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
336};
337pub use tool_call_repair::{
338 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
339 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
340 salvage_tool_arguments, tool_call_repair_capability,
341};
342pub use tool_output_distillation::{
343 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
344};
345pub use tool_output_persistence::{
346 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
347};
348pub use tool_search::{
349 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
350};
351pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
352#[cfg(feature = "web-fetch")]
353pub use web_fetch::{
354 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
355 derive_bot_auth_public_key,
356};
357
358pub struct SystemPromptContext {
368 pub session_id: SessionId,
370 pub locale: Option<String>,
372 pub file_store: Option<Arc<dyn SessionFileSystem>>,
374 pub model: Option<String>,
380}
381
382impl SystemPromptContext {
383 pub fn without_file_store(session_id: SessionId) -> Self {
385 Self {
386 session_id,
387 locale: None,
388 file_store: None,
389 model: None,
390 }
391 }
392
393 pub fn with_model(mut self, model: impl Into<String>) -> Self {
395 self.model = Some(model.into());
396 self
397 }
398}
399
400#[derive(Debug, Clone)]
452pub struct CapabilityLocalization {
453 pub locale: &'static str,
455 pub name: Option<&'static str>,
457 pub description: Option<&'static str>,
459 pub config_description: Option<&'static str>,
464 pub config_overlay: Option<serde_json::Value>,
470}
471
472impl CapabilityLocalization {
473 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
475 Self {
476 locale,
477 name: Some(name),
478 description: Some(description),
479 config_description: None,
480 config_overlay: None,
481 }
482 }
483}
484
485pub fn resolve_localized_field<T>(
489 localizations: &[CapabilityLocalization],
490 locale: Option<&str>,
491 field: impl Fn(&CapabilityLocalization) -> Option<T>,
492) -> Option<T> {
493 let mut candidates: Vec<String> = Vec::new();
494 if let Some(raw) = locale {
495 let normalized = raw.trim().replace('_', "-").to_lowercase();
496 if !normalized.is_empty() {
497 if let Some((language, _)) = normalized.split_once('-') {
498 let language = language.to_string();
499 candidates.push(normalized);
500 candidates.push(language);
501 } else {
502 candidates.push(normalized);
503 }
504 }
505 }
506 candidates.push("en".to_string());
507
508 for candidate in candidates {
509 let hit = localizations
510 .iter()
511 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
512 .and_then(&field);
513 if hit.is_some() {
514 return hit;
515 }
516 }
517 None
518}
519
520#[async_trait]
521pub trait Capability: Send + Sync {
522 fn id(&self) -> &str;
524
525 fn aliases(&self) -> Vec<&'static str> {
534 vec![]
535 }
536
537 fn name(&self) -> &str;
539
540 fn description(&self) -> &str;
542
543 fn localizations(&self) -> Vec<CapabilityLocalization> {
548 vec![]
549 }
550
551 fn localized_name(&self, locale: Option<&str>) -> String {
554 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
555 .unwrap_or_else(|| self.name())
556 .to_string()
557 }
558
559 fn localized_description(&self, locale: Option<&str>) -> String {
561 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
562 .unwrap_or_else(|| self.description())
563 .to_string()
564 }
565
566 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
570 resolve_localized_field(&self.localizations(), locale, |entry| {
571 entry.config_description
572 })
573 .map(str::to_string)
574 }
575
576 fn status(&self) -> CapabilityStatus {
578 CapabilityStatus::Available
579 }
580
581 fn icon(&self) -> Option<&str> {
583 None
584 }
585
586 fn category(&self) -> Option<&str> {
588 None
589 }
590
591 fn is_guardrail(&self) -> bool {
596 false
597 }
598
599 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
610 None
611 }
612
613 fn system_prompt_addition(&self) -> Option<&str> {
633 None
634 }
635
636 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
648 self.system_prompt_addition().map(|addition| {
649 format!(
650 "<capability id=\"{}\">\n{}\n</capability>",
651 self.id(),
652 addition
653 )
654 })
655 }
656
657 fn system_prompt_preview(&self) -> Option<String> {
663 self.system_prompt_addition().map(|s| s.to_string())
664 }
665
666 fn tools(&self) -> Vec<Box<dyn Tool>> {
668 vec![]
669 }
670
671 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
679 self.tools()
680 }
681
682 async fn system_prompt_contribution_with_config(
689 &self,
690 ctx: &SystemPromptContext,
691 _config: &serde_json::Value,
692 ) -> Option<String> {
693 self.system_prompt_contribution(ctx).await
694 }
695
696 fn tool_definitions(&self) -> Vec<ToolDefinition> {
699 self.tools().iter().map(|t| t.to_definition()).collect()
700 }
701
702 fn mounts(&self) -> Vec<MountPoint> {
710 vec![]
711 }
712
713 fn dependencies(&self) -> Vec<&'static str> {
722 vec![]
723 }
724
725 fn features(&self) -> Vec<&'static str> {
740 vec![]
741 }
742
743 fn config_schema(&self) -> Option<serde_json::Value> {
749 None
750 }
751
752 fn config_ui_schema(&self) -> Option<serde_json::Value> {
757 None
758 }
759
760 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
766 Ok(())
767 }
768
769 fn mcp_servers(&self) -> ScopedMcpServers {
775 ScopedMcpServers::default()
776 }
777
778 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
780 self.mcp_servers()
781 }
782
783 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
796 None
797 }
798
799 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
807 None
808 }
809
810 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
821 vec![]
822 }
823
824 fn pre_tool_use_hooks_with_config(
829 &self,
830 _config: &serde_json::Value,
831 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
832 self.pre_tool_use_hooks()
833 }
834
835 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
843 vec![]
844 }
845
846 fn post_tool_exec_hooks_with_config(
851 &self,
852 _config: &serde_json::Value,
853 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
854 self.post_tool_exec_hooks()
855 }
856
857 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
866 vec![]
867 }
868
869 fn tool_definition_hooks_with_config(
874 &self,
875 _config: &serde_json::Value,
876 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
877 self.tool_definition_hooks()
878 }
879
880 fn tool_definition_hooks_with_context(
890 &self,
891 _ctx: &SystemPromptContext,
892 config: &serde_json::Value,
893 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
894 self.tool_definition_hooks_with_config(config)
895 }
896
897 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
905 vec![]
906 }
907
908 fn narrate(
922 &self,
923 _tool_def: Option<&ToolDefinition>,
924 tool_call: &ToolCall,
925 phase: crate::tool_narration::ToolNarrationPhase,
926 locale: Option<&str>,
927 ) -> Option<String> {
928 self.tools()
929 .iter()
930 .find(|tool| tool.name() == tool_call.name)
931 .and_then(|tool| tool.narrate(tool_call, phase, locale))
932 }
933
934 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
950 vec![]
951 }
952
953 fn user_hooks_with_config(
959 &self,
960 _config: &serde_json::Value,
961 ) -> Vec<crate::user_hook_types::UserHookSpec> {
962 self.user_hooks()
963 }
964
965 fn risk_level(&self) -> RiskLevel {
973 RiskLevel::Low
974 }
975
976 fn commands(&self) -> Vec<CommandDescriptor> {
984 vec![]
985 }
986
987 async fn execute_command(
1001 &self,
1002 request: &ExecuteCommandRequest,
1003 _ctx: &CommandExecutionContext,
1004 ) -> crate::error::Result<CommandResult> {
1005 Err(crate::error::AgentLoopError::config(format!(
1006 "capability {} declared command /{} but does not implement execute_command",
1007 self.id(),
1008 request.name,
1009 )))
1010 }
1011
1012 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1020 vec![]
1021 }
1022
1023 fn contribute_skills(&self) -> Vec<SkillContribution> {
1033 vec![]
1034 }
1035
1036 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1047 vec![]
1048 }
1049
1050 fn post_output_guardrails_with_config(
1062 &self,
1063 _config: &serde_json::Value,
1064 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1065 vec![]
1066 }
1067}
1068
1069pub trait ToolDefinitionHook: Send + Sync {
1070 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1071
1072 fn applies_with_native_tool_search(&self) -> bool {
1077 true
1078 }
1079}
1080
1081pub trait ToolCallHook: Send + Sync {
1082 fn narration(
1083 &self,
1084 _tool_def: Option<&ToolDefinition>,
1085 _tool_call: &ToolCall,
1086 _phase: crate::tool_narration::ToolNarrationPhase,
1087 _locale: Option<&str>,
1088 ) -> Option<String> {
1089 None
1090 }
1091
1092 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1093 tool_call
1094 }
1095}
1096
1097pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1103
1104impl ToolCallHook for CapabilityNarrationHook {
1105 fn narration(
1106 &self,
1107 tool_def: Option<&ToolDefinition>,
1108 tool_call: &ToolCall,
1109 phase: crate::tool_narration::ToolNarrationPhase,
1110 locale: Option<&str>,
1111 ) -> Option<String> {
1112 self.0.narrate(tool_def, tool_call, phase, locale)
1113 }
1114}
1115
1116#[derive(
1120 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1121)]
1122#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1123#[cfg_attr(feature = "openapi", schema(example = "low"))]
1124#[serde(rename_all = "lowercase")]
1125pub enum RiskLevel {
1126 Low,
1128 Medium,
1130 High,
1132}
1133
1134#[derive(Debug, Clone, Serialize, Deserialize)]
1140#[serde(rename_all = "snake_case")]
1141pub enum BlueprintModel {
1142 Fixed(String),
1144 Default(String),
1146 Inherit,
1148}
1149
1150pub struct AgentBlueprint {
1156 pub id: &'static str,
1158 pub name: &'static str,
1160 pub description: &'static str,
1162 pub model: BlueprintModel,
1164 pub system_prompt: &'static str,
1166 pub tools: Vec<Box<dyn Tool>>,
1168 pub max_turns: Option<usize>,
1170 pub config_schema: Option<serde_json::Value>,
1172}
1173
1174impl AgentBlueprint {
1175 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1177 self.tools.iter().map(|t| t.to_definition()).collect()
1178 }
1179}
1180
1181impl std::fmt::Debug for AgentBlueprint {
1182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1183 f.debug_struct("AgentBlueprint")
1184 .field("id", &self.id)
1185 .field("name", &self.name)
1186 .field("model", &self.model)
1187 .field("tool_count", &self.tools.len())
1188 .field("max_turns", &self.max_turns)
1189 .finish()
1190 }
1191}
1192
1193#[derive(Clone)]
1220pub struct CapabilityRegistry {
1221 capabilities: HashMap<String, Arc<dyn Capability>>,
1222 aliases: HashMap<String, String>,
1224}
1225
1226impl CapabilityRegistry {
1227 pub fn new() -> Self {
1229 Self {
1230 capabilities: HashMap::new(),
1231 aliases: HashMap::new(),
1232 }
1233 }
1234
1235 pub fn with_builtins() -> Self {
1240 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1241 }
1242
1243 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1248 let mut registry = Self::new();
1249
1250 registry.register(AgentInstructionsCapability);
1252 registry.register(HumanIntentCapability);
1253 registry.register(NoopCapability);
1254 registry.register(CurrentTimeCapability);
1255 registry.register(MessageMetadataCapability);
1256 registry.register(ResearchCapability);
1257 registry.register(ModelScoutCapability);
1258 registry.register(OpenRouterWorkspaceCapability);
1259 registry.register(OpenRouterServerToolsCapability);
1260 registry.register(PlatformManagementCapability);
1261 registry.register(FileSystemCapability);
1262 registry.register(MemoryCapability);
1263 registry.register(SessionStorageCapability);
1264 registry.register(SessionCapability);
1265 registry.register(SessionSqlDatabaseCapability);
1266 registry.register(TestMathCapability);
1267 registry.register(TestWeatherCapability);
1268 registry.register(StatelessTodoListCapability);
1269 #[cfg(feature = "web-fetch")]
1270 registry.register(WebFetchCapability::from_env());
1271 registry.register(BashkitShellCapability);
1272 registry.register(BackgroundExecutionCapability);
1273 registry.register(SessionScheduleCapability);
1274 registry.register(BtwCapability);
1275 registry.register(InfinityContextCapability);
1276 registry.register(budgeting::BudgetingCapability);
1277 registry.register(SelfBudgetCapability);
1278 registry.register(CompactionCapability);
1279 registry.register(ErrorDisclosureCapability);
1280
1281 registry.register(OpenAiToolSearchCapability::new());
1283 registry.register(ClaudeToolSearchCapability::new());
1285 registry.register(ToolSearchCapability::new());
1287 registry.register(AutoToolSearchCapability::new());
1289 registry.register(PromptCachingCapability::new());
1290
1291 registry.register(ParallelToolCallsCapability);
1293
1294 registry.register(SkillsCapability);
1296
1297 registry.register(SubagentCapability);
1299
1300 registry.register(SessionTasksCapability);
1302
1303 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1307 registry.register(AgentHandoffCapability);
1308 #[cfg(feature = "a2a")]
1312 registry.register(A2aAgentDelegationCapability);
1313 }
1314
1315 registry.register(SystemCommandsCapability);
1317
1318 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1320 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1321
1322 registry.register(user_hooks::UserHooksCapability);
1325
1326 registry.register(LoopDetectionCapability);
1328
1329 registry.register(ToolCallRepairCapability);
1333
1334 registry.register(PromptCanaryGuardrailCapability);
1337
1338 registry.register(GuardrailsCapability);
1341
1342 #[cfg(feature = "ui-capabilities")]
1344 {
1345 registry.register(OpenUiCapability);
1346 registry.register(A2UiCapability);
1347 }
1348
1349 registry.register(SampleDataCapability);
1351
1352 registry.register(DataKnowledgeCapability);
1354
1355 registry.register(KnowledgeBaseCapability);
1357
1358 registry.register(KnowledgeIndexCapability);
1360
1361 registry.register(FakeWarehouseCapability);
1363 registry.register(FakeAwsCapability);
1364 registry.register(FakeCrmCapability);
1365 registry.register(FakeFinancialCapability);
1366
1367 let internal_flags = crate::InternalFeatureFlags::from_env();
1369 if internal_flags.session_sandbox {
1370 registry.register(SessionSandboxCapability);
1371 }
1372
1373 if internal_flags.lua {
1377 registry.register(LuaCapability);
1378 registry.register(LuaCodeModeCapability);
1381 }
1382 for plugin in inventory::iter::<IntegrationPlugin>() {
1383 if (!plugin.experimental_only || grade.experimental_features_enabled())
1384 && plugin
1385 .feature_flag
1386 .is_none_or(|f| internal_flags.is_enabled(f))
1387 {
1388 registry.register_boxed((plugin.factory)());
1389 }
1390 }
1391
1392 registry
1393 }
1394
1395 pub fn register(&mut self, capability: impl Capability + 'static) {
1397 self.register_arc(Arc::new(capability));
1398 }
1399
1400 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1402 self.register_arc(Arc::from(capability));
1403 }
1404
1405 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1407 let canonical = capability.id().to_string();
1408 for alias in capability.aliases() {
1409 self.aliases.insert(alias.to_string(), canonical.clone());
1410 }
1411 self.capabilities.insert(canonical, capability);
1412 }
1413
1414 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1416 self.capabilities
1417 .get(id)
1418 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1419 }
1420
1421 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1426 if self.capabilities.contains_key(id) {
1427 Some(id)
1428 } else {
1429 self.aliases
1430 .get(id)
1431 .filter(|c| self.capabilities.contains_key(*c))
1432 .map(String::as_str)
1433 }
1434 }
1435
1436 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1438 let canonical = self.canonical_id(id)?.to_string();
1439 let removed = self.capabilities.remove(&canonical);
1440 self.aliases.retain(|_, target| *target != canonical);
1441 removed
1442 }
1443
1444 pub fn has(&self, id: &str) -> bool {
1446 self.get(id).is_some()
1447 }
1448
1449 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1451 self.capabilities.values().collect()
1452 }
1453
1454 pub fn len(&self) -> usize {
1456 self.capabilities.len()
1457 }
1458
1459 pub fn is_empty(&self) -> bool {
1461 self.capabilities.is_empty()
1462 }
1463
1464 pub fn builder() -> CapabilityRegistryBuilder {
1466 CapabilityRegistryBuilder::new()
1467 }
1468
1469 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1473 for cap in self.capabilities.values() {
1474 for bp in cap.agent_blueprints() {
1475 if bp.id == id {
1476 return Some(bp);
1477 }
1478 }
1479 }
1480 None
1481 }
1482
1483 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1487 for (capability_id, cap) in &self.capabilities {
1488 for bp in cap.agent_blueprints() {
1489 if bp.id == id {
1490 return Some((capability_id.clone(), bp));
1491 }
1492 }
1493 }
1494 None
1495 }
1496
1497 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1499 self.capabilities
1500 .values()
1501 .flat_map(|cap| cap.agent_blueprints())
1502 .collect()
1503 }
1504}
1505
1506impl Default for CapabilityRegistry {
1507 fn default() -> Self {
1508 Self::with_builtins()
1509 }
1510}
1511
1512impl std::fmt::Debug for CapabilityRegistry {
1513 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1514 let ids: Vec<_> = self.capabilities.keys().collect();
1515 f.debug_struct("CapabilityRegistry")
1516 .field("capabilities", &ids)
1517 .finish()
1518 }
1519}
1520
1521pub struct CapabilityRegistryBuilder {
1523 registry: CapabilityRegistry,
1524}
1525
1526impl CapabilityRegistryBuilder {
1527 pub fn new() -> Self {
1529 Self {
1530 registry: CapabilityRegistry::new(),
1531 }
1532 }
1533
1534 pub fn with_builtins() -> Self {
1536 Self {
1537 registry: CapabilityRegistry::with_builtins(),
1538 }
1539 }
1540
1541 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1543 self.registry.register(capability);
1544 self
1545 }
1546
1547 pub fn build(self) -> CapabilityRegistry {
1549 self.registry
1550 }
1551}
1552
1553impl Default for CapabilityRegistryBuilder {
1554 fn default() -> Self {
1555 Self::new()
1556 }
1557}
1558
1559pub struct ModelViewContext<'a> {
1565 pub session_id: SessionId,
1566 pub prior_usage: Option<&'a TokenUsage>,
1567}
1568
1569pub trait ModelViewProvider: Send + Sync {
1575 fn apply_model_view(
1576 &self,
1577 messages: Vec<Message>,
1578 config: &serde_json::Value,
1579 context: &ModelViewContext<'_>,
1580 ) -> Vec<Message>;
1581
1582 fn priority(&self) -> i32 {
1583 0
1584 }
1585}
1586
1587pub struct CollectedCapabilities {
1592 pub system_prompt_parts: Vec<String>,
1594 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1596 pub tools: Vec<Box<dyn Tool>>,
1598 pub tool_definitions: Vec<ToolDefinition>,
1600 pub mounts: Vec<MountPoint>,
1602 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1604 pub applied_ids: Vec<String>,
1606 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1608 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1610 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1613 pub parallel_tool_calls: Option<bool>,
1617 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1619 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1621 pub mcp_servers: ScopedMcpServers,
1623 }
1629
1630#[derive(Debug, Clone, PartialEq, Eq)]
1631pub struct SystemPromptAttribution {
1632 pub capability_id: String,
1633 pub content: String,
1634}
1635
1636impl CollectedCapabilities {
1637 pub fn system_prompt_prefix(&self) -> Option<String> {
1640 if self.system_prompt_parts.is_empty() {
1641 None
1642 } else {
1643 Some(self.system_prompt_parts.join("\n\n"))
1644 }
1645 }
1646
1647 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1651 for (provider, config) in &self.message_filter_providers {
1653 provider.apply_filters(query, config);
1654 }
1655 }
1656
1657 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1660 for (provider, config) in &self.message_filter_providers {
1661 provider.post_load(messages, config);
1662 }
1663 }
1664
1665 pub fn has_message_filters(&self) -> bool {
1667 !self.message_filter_providers.is_empty()
1668 }
1669}
1670
1671pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1676 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1677 return base_system_prompt.to_string();
1678 };
1679
1680 if base_system_prompt.is_empty() {
1681 return additions.to_string();
1682 }
1683
1684 if base_system_prompt.contains("<system-prompt>") {
1685 format!("{base_system_prompt}\n\n{additions}")
1686 } else {
1687 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1688 }
1689}
1690
1691pub struct CollectedMessageFilters {
1698 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1700}
1701
1702pub struct CollectedModelViewProviders {
1704 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1706}
1707
1708impl CollectedMessageFilters {
1714 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1716 for (provider, config) in &self.message_filter_providers {
1717 provider.apply_filters(query, config);
1718 }
1719 }
1720
1721 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1723 for (provider, config) in &self.message_filter_providers {
1724 provider.post_load(messages, config);
1725 }
1726 }
1727}
1728
1729impl CollectedModelViewProviders {
1730 pub fn apply_model_view(
1732 &self,
1733 mut messages: Vec<Message>,
1734 context: &ModelViewContext<'_>,
1735 ) -> Vec<Message> {
1736 for (provider, config) in &self.model_view_providers {
1737 messages = provider.apply_model_view(messages, config, context);
1738 }
1739 messages
1740 }
1741}
1742
1743fn compaction_is_enabled(
1749 capability_configs: &[AgentCapabilityConfig],
1750 registry: &CapabilityRegistry,
1751) -> bool {
1752 capability_configs.iter().any(|cap_config| {
1753 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
1754 && registry
1755 .get(cap_config.capability_ref.as_str())
1756 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
1757 })
1758}
1759
1760fn message_filter_config_for(
1769 cap_id: &str,
1770 base: &serde_json::Value,
1771 compaction_on: bool,
1772) -> serde_json::Value {
1773 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
1774 return base.clone();
1775 }
1776 let mut config = base.clone();
1777 match config.as_object_mut() {
1778 Some(map) => {
1779 map.insert(
1780 "compaction_active".to_string(),
1781 serde_json::Value::Bool(true),
1782 );
1783 }
1784 None => {
1785 config = serde_json::json!({ "compaction_active": true });
1786 }
1787 }
1788 config
1789}
1790
1791pub fn collect_message_filters_only(
1797 capability_configs: &[AgentCapabilityConfig],
1798 registry: &CapabilityRegistry,
1799) -> CollectedMessageFilters {
1800 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1801 Vec::new();
1802 let compaction_on = compaction_is_enabled(capability_configs, registry);
1803
1804 for cap_config in capability_configs {
1805 let cap_id = cap_config.capability_ref.as_str();
1806 if let Some(capability) = registry.get(cap_id) {
1807 if capability.status() != CapabilityStatus::Available {
1808 continue;
1809 }
1810 let effective: &dyn Capability = capability
1813 .resolve_for_model(None)
1814 .unwrap_or_else(|| capability.as_ref());
1815 if let Some(provider) = effective.message_filter_provider() {
1816 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
1817 message_filter_providers.push((provider, config));
1818 }
1819 }
1820 }
1821
1822 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1823
1824 CollectedMessageFilters {
1825 message_filter_providers,
1826 }
1827}
1828
1829pub fn collect_model_view_providers(
1836 capability_configs: &[AgentCapabilityConfig],
1837 registry: &CapabilityRegistry,
1838 model: Option<&str>,
1839) -> CollectedModelViewProviders {
1840 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1841
1842 for cap_config in capability_configs {
1843 let cap_id = cap_config.capability_ref.as_str();
1844 if let Some(capability) = registry.get(cap_id) {
1845 if capability.status() != CapabilityStatus::Available {
1846 continue;
1847 }
1848 let effective: &dyn Capability = capability
1849 .resolve_for_model(model)
1850 .unwrap_or_else(|| capability.as_ref());
1851 if let Some(provider) = effective.model_view_provider() {
1852 model_view_providers.push((provider, cap_config.config.clone()));
1853 }
1854 }
1855 }
1856
1857 model_view_providers.sort_by_key(|(p, _)| p.priority());
1858
1859 CollectedModelViewProviders {
1860 model_view_providers,
1861 }
1862}
1863
1864pub fn collect_capability_mcp_servers(
1865 capability_configs: &[AgentCapabilityConfig],
1866 registry: &CapabilityRegistry,
1867) -> ScopedMcpServers {
1868 let mut servers = ScopedMcpServers::default();
1869
1870 for cap_config in capability_configs {
1871 let cap_id = cap_config.capability_ref.as_str();
1872 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
1875 if let Ok(definition) =
1876 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1877 {
1878 if definition.status != CapabilityStatus::Available {
1879 continue;
1880 }
1881 if let Some(contributed) = definition.mcp_servers {
1882 servers = merge_scoped_mcp_servers(&servers, &contributed);
1883 }
1884 }
1885 continue;
1886 }
1887 if let Some(capability) = registry.get(cap_id) {
1888 if capability.status() != CapabilityStatus::Available {
1889 continue;
1890 }
1891 servers = merge_scoped_mcp_servers(
1892 &servers,
1893 &capability.mcp_servers_with_config(&cap_config.config),
1894 );
1895 }
1896 }
1897
1898 servers
1899}
1900
1901pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1908
1909#[derive(Debug, Clone, PartialEq, Eq)]
1911pub enum DependencyError {
1912 CircularDependency {
1914 capability_id: String,
1916 chain: Vec<String>,
1918 },
1919 TooManyCapabilities {
1921 count: usize,
1923 max: usize,
1925 },
1926}
1927
1928impl std::fmt::Display for DependencyError {
1929 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1930 match self {
1931 DependencyError::CircularDependency {
1932 capability_id,
1933 chain,
1934 } => {
1935 write!(
1936 f,
1937 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1938 capability_id,
1939 chain.join(" -> "),
1940 capability_id
1941 )
1942 }
1943 DependencyError::TooManyCapabilities { count, max } => {
1944 write!(
1945 f,
1946 "Too many capabilities after resolution: {} (max: {})",
1947 count, max
1948 )
1949 }
1950 }
1951 }
1952}
1953
1954impl std::error::Error for DependencyError {}
1955
1956#[derive(Debug, Clone)]
1958pub struct ResolvedCapabilities {
1959 pub resolved_ids: Vec<String>,
1962 pub added_as_dependencies: Vec<String>,
1964 pub user_selected: Vec<String>,
1966}
1967
1968pub fn resolve_dependencies(
1988 selected_ids: &[String],
1989 registry: &CapabilityRegistry,
1990) -> Result<ResolvedCapabilities, DependencyError> {
1991 use std::collections::HashSet;
1992
1993 let user_selected: HashSet<String> = selected_ids
1995 .iter()
1996 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
1997 .collect();
1998 let mut resolved: Vec<String> = Vec::new();
1999 let mut resolved_set: HashSet<String> = HashSet::new();
2000 let mut added_as_dependencies: Vec<String> = Vec::new();
2001
2002 for cap_id in selected_ids {
2004 resolve_single_capability(
2005 cap_id,
2006 registry,
2007 &mut resolved,
2008 &mut resolved_set,
2009 &mut added_as_dependencies,
2010 &user_selected,
2011 &mut Vec::new(), )?;
2013 }
2014
2015 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2017 return Err(DependencyError::TooManyCapabilities {
2018 count: resolved.len(),
2019 max: MAX_RESOLVED_CAPABILITIES,
2020 });
2021 }
2022
2023 Ok(ResolvedCapabilities {
2024 resolved_ids: resolved,
2025 added_as_dependencies,
2026 user_selected: selected_ids.to_vec(),
2027 })
2028}
2029
2030pub fn resolve_capability_configs(
2035 selected_configs: &[AgentCapabilityConfig],
2036 registry: &CapabilityRegistry,
2037) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2038 let mut selected_ids: Vec<String> = Vec::new();
2039 for config in selected_configs {
2040 if (is_declarative_capability(config.capability_id())
2043 || is_plugin_capability(config.capability_id()))
2044 && let Ok(definition) =
2045 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2046 {
2047 selected_ids.extend(definition.dependencies);
2048 }
2049 selected_ids.push(config.capability_id().to_string());
2050 }
2051 let resolved = resolve_dependencies(&selected_ids, registry)?;
2052
2053 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2056 .iter()
2057 .map(|config| {
2058 let id = config.capability_id();
2059 let id = registry.canonical_id(id).unwrap_or(id);
2060 (id.to_string(), config.config.clone())
2061 })
2062 .collect();
2063
2064 Ok(resolved
2065 .resolved_ids
2066 .into_iter()
2067 .map(|capability_id| {
2068 explicit_configs
2069 .get(&capability_id)
2070 .cloned()
2071 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2072 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2073 })
2074 .collect())
2075}
2076
2077fn resolve_single_capability(
2079 cap_id: &str,
2080 registry: &CapabilityRegistry,
2081 resolved: &mut Vec<String>,
2082 resolved_set: &mut std::collections::HashSet<String>,
2083 added_as_dependencies: &mut Vec<String>,
2084 user_selected: &std::collections::HashSet<String>,
2085 visiting: &mut Vec<String>,
2086) -> Result<(), DependencyError> {
2087 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2091
2092 if resolved_set.contains(cap_id) {
2094 return Ok(());
2095 }
2096
2097 if visiting.contains(&cap_id.to_string()) {
2099 return Err(DependencyError::CircularDependency {
2100 capability_id: cap_id.to_string(),
2101 chain: visiting.clone(),
2102 });
2103 }
2104
2105 let capability = match registry.get(cap_id) {
2107 Some(cap) => cap,
2108 None => {
2109 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2113 && !resolved_set.contains(cap_id)
2114 {
2115 resolved.push(cap_id.to_string());
2116 resolved_set.insert(cap_id.to_string());
2117 if !user_selected.contains(cap_id) {
2118 added_as_dependencies.push(cap_id.to_string());
2119 }
2120 }
2121 return Ok(());
2122 }
2123 };
2124
2125 visiting.push(cap_id.to_string());
2127
2128 for dep_id in capability.dependencies() {
2130 resolve_single_capability(
2131 dep_id,
2132 registry,
2133 resolved,
2134 resolved_set,
2135 added_as_dependencies,
2136 user_selected,
2137 visiting,
2138 )?;
2139 }
2140
2141 visiting.pop();
2143
2144 if !resolved_set.contains(cap_id) {
2146 resolved.push(cap_id.to_string());
2147 resolved_set.insert(cap_id.to_string());
2148
2149 if !user_selected.contains(cap_id) {
2151 added_as_dependencies.push(cap_id.to_string());
2152 }
2153 }
2154
2155 Ok(())
2156}
2157
2158pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2163 use std::collections::HashSet;
2164
2165 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2166 Ok(resolved) => resolved.resolved_ids,
2167 Err(_) => capability_ids.to_vec(),
2168 };
2169
2170 let mut seen = HashSet::new();
2171 let mut features = Vec::new();
2172 for cap_id in &resolved_ids {
2173 if let Some(cap) = registry.get(cap_id) {
2174 for feature in cap.features() {
2175 if seen.insert(feature) {
2176 features.push(feature.to_string());
2177 }
2178 }
2179 }
2180 }
2181 features
2182}
2183
2184pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2187 registry
2188 .get(cap_id)
2189 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2190 .unwrap_or_default()
2191}
2192
2193pub async fn collect_capabilities(
2209 capability_ids: &[String],
2210 registry: &CapabilityRegistry,
2211 ctx: &SystemPromptContext,
2212) -> CollectedCapabilities {
2213 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2216 Ok(resolved) => resolved.resolved_ids,
2217 Err(e) => {
2218 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2219 capability_ids.to_vec()
2220 }
2221 };
2222
2223 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2225 .iter()
2226 .map(|id| AgentCapabilityConfig {
2227 capability_ref: CapabilityId::new(id),
2228 config: serde_json::Value::Object(serde_json::Map::new()),
2229 })
2230 .collect();
2231
2232 collect_capabilities_with_configs(&configs, registry, ctx).await
2233}
2234
2235pub async fn collect_capabilities_with_configs(
2246 capability_configs: &[AgentCapabilityConfig],
2247 registry: &CapabilityRegistry,
2248 ctx: &SystemPromptContext,
2249) -> CollectedCapabilities {
2250 let mut system_prompt_parts: Vec<String> = Vec::new();
2251 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2252 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2253 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2254 let mut mounts: Vec<MountPoint> = Vec::new();
2255 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2256 Vec::new();
2257 let mut applied_ids: Vec<String> = Vec::new();
2258 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2259 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2260 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2261 let mut parallel_tool_calls: Option<bool> = None;
2262 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2263 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2264 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2267 let mut mcp_servers = ScopedMcpServers::default();
2268 let compaction_on = compaction_is_enabled(capability_configs, registry);
2269
2270 for cap_config in capability_configs {
2271 let cap_id = cap_config.capability_ref.as_str();
2272 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2277 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2278 cap_config.config.clone(),
2279 ) {
2280 Ok(definition) => {
2281 if definition.status != CapabilityStatus::Available {
2282 continue;
2283 }
2284
2285 if let Some(prompt) = definition.system_prompt.as_deref() {
2286 let contribution =
2287 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2288 system_prompt_attributions.push(SystemPromptAttribution {
2289 capability_id: cap_id.to_string(),
2290 content: contribution.clone(),
2291 });
2292 system_prompt_parts.push(contribution);
2293 }
2294
2295 mounts.extend(definition.mounts(cap_id));
2296 if let Some(ref servers) = definition.mcp_servers {
2297 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2298 }
2299 for skill in definition.skill_contributions() {
2300 mounts.push(skill.to_mount(cap_id));
2301 }
2302
2303 applied_ids.push(cap_id.to_string());
2304 }
2305 Err(error) => {
2306 tracing::warn!(
2307 capability_id = %cap_id,
2308 error = %error,
2309 "Skipping invalid declarative/plugin capability config"
2310 );
2311 }
2312 }
2313 continue;
2314 }
2315 if let Some(capability) = registry.get(cap_id) {
2316 if capability.status() != CapabilityStatus::Available {
2318 continue;
2319 }
2320
2321 let effective: &dyn Capability =
2333 match capability.resolve_for_model(ctx.model.as_deref()) {
2334 Some(inner) => inner,
2335 None => capability.as_ref(),
2336 };
2337 let effective_id = effective.id();
2338
2339 if let Some(contribution) = effective
2341 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2342 .await
2343 {
2344 system_prompt_attributions.push(SystemPromptAttribution {
2345 capability_id: cap_id.to_string(),
2346 content: contribution.clone(),
2347 });
2348 system_prompt_parts.push(contribution);
2349 }
2350
2351 tools.extend(effective.tools_with_config(&cap_config.config));
2353 tool_definition_hooks
2354 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2355 tool_call_hooks.extend(effective.tool_call_hooks());
2356 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2358 let cap_category = effective.category();
2363 for def in effective.tool_definitions() {
2364 let def = match (def.category(), cap_category) {
2365 (None, Some(cat)) => def.with_category(cat),
2366 _ => def,
2367 }
2368 .with_capability_attribution(cap_id, Some(capability.name()));
2369 tool_definitions.push(def);
2370 }
2371
2372 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2380 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2381 {
2382 let threshold = cap_config
2384 .config
2385 .get("threshold")
2386 .and_then(|v| v.as_u64())
2387 .map(|v| v as usize)
2388 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2389 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2390 enabled: true,
2391 threshold,
2392 });
2393 }
2394
2395 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2396 let strategy = cap_config
2397 .config
2398 .get("strategy")
2399 .and_then(|v| v.as_str())
2400 .map(|value| match value {
2401 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2402 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2403 })
2404 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2405 let gemini_cached_content = cap_config
2406 .config
2407 .get("gemini_cached_content")
2408 .and_then(|v| v.as_str())
2409 .map(str::to_string);
2410 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2411 enabled: true,
2412 strategy,
2413 gemini_cached_content,
2414 });
2415 }
2416
2417 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2418 parallel_tool_calls =
2419 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2420 }
2421
2422 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2423 let server_tools =
2424 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2425 if !server_tools.is_empty() {
2426 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2427 server_tools,
2428 ..Default::default()
2429 });
2430 }
2431 }
2432
2433 mounts.extend(effective.mounts());
2435
2436 mcp_servers = merge_scoped_mcp_servers(
2437 &mcp_servers,
2438 &effective.mcp_servers_with_config(&cap_config.config),
2439 );
2440
2441 for skill in effective.contribute_skills() {
2445 mounts.push(skill.to_mount(cap_id));
2446 }
2447
2448 if let Some(provider) = effective.message_filter_provider() {
2450 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2451 message_filter_providers.push((provider, config));
2452 }
2453
2454 applied_ids.push(cap_id.to_string());
2455 }
2456 }
2457
2458 if !applied_ids
2470 .iter()
2471 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2472 && tool_definitions
2473 .iter()
2474 .any(|def| def.hints().supports_background == Some(true))
2475 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2476 && bg_cap.status() == CapabilityStatus::Available
2477 {
2478 tools.extend(bg_cap.tools());
2479 let cap_category = bg_cap.category();
2480 for def in bg_cap.tool_definitions() {
2481 let def = match (def.category(), cap_category) {
2482 (None, Some(cat)) => def.with_category(cat),
2483 _ => def,
2484 }
2485 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2486 tool_definitions.push(def);
2487 }
2488 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2489 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2490 }
2491
2492 tool_call_hooks.extend(narration_hooks);
2496
2497 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2499
2500 CollectedCapabilities {
2501 system_prompt_parts,
2502 system_prompt_attributions,
2503 tools,
2504 tool_definitions,
2505 mounts,
2506 message_filter_providers,
2507 applied_ids,
2508 tool_search,
2509 prompt_cache,
2510 openrouter_routing,
2511 parallel_tool_calls,
2512 tool_definition_hooks,
2513 tool_call_hooks,
2514 mcp_servers,
2515 }
2516}
2517
2518pub struct AppliedCapabilities {
2524 pub runtime_agent: RuntimeAgent,
2526 pub tool_registry: ToolRegistry,
2528 pub applied_ids: Vec<String>,
2530}
2531
2532pub async fn apply_capabilities(
2569 base_runtime_agent: RuntimeAgent,
2570 capability_ids: &[String],
2571 registry: &CapabilityRegistry,
2572 ctx: &SystemPromptContext,
2573) -> AppliedCapabilities {
2574 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2575
2576 let final_system_prompt = compose_system_prompt(
2578 &base_runtime_agent.system_prompt,
2579 collected.system_prompt_prefix().as_deref(),
2580 );
2581
2582 let mut tool_registry = ToolRegistry::new();
2584 for tool in collected.tools {
2585 tool_registry.register_boxed(tool);
2586 }
2587
2588 let mut tools = collected.tool_definitions;
2590 for hook in &collected.tool_definition_hooks {
2591 tools = hook.transform(tools);
2592 }
2593
2594 let runtime_agent = RuntimeAgent {
2595 system_prompt: final_system_prompt,
2596 model: base_runtime_agent.model,
2597 tools,
2598 max_iterations: base_runtime_agent.max_iterations,
2599 temperature: base_runtime_agent.temperature,
2600 max_tokens: base_runtime_agent.max_tokens,
2601 tool_search: collected.tool_search,
2602 prompt_cache: collected.prompt_cache,
2603 openrouter_routing: collected.openrouter_routing,
2604 network_access: base_runtime_agent.network_access,
2605 parallel_tool_calls: base_runtime_agent
2608 .parallel_tool_calls
2609 .or(collected.parallel_tool_calls),
2610 };
2611
2612 AppliedCapabilities {
2613 runtime_agent,
2614 tool_registry,
2615 applied_ids: collected.applied_ids,
2616 }
2617}
2618
2619#[cfg(test)]
2624mod tests {
2625 use super::*;
2626 use crate::typed_id::SessionId;
2627 use std::collections::BTreeSet;
2628 use uuid::Uuid;
2629
2630 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2632
2633 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2634 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2635 }
2636
2637 fn test_ctx() -> SystemPromptContext {
2639 SystemPromptContext::without_file_store(SessionId::new())
2640 }
2641
2642 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2644 let mut ids = [
2645 "agent_instructions",
2646 "human_intent",
2647 "budgeting",
2648 "self_budget",
2649 "noop",
2650 "current_time",
2651 "research",
2652 "platform_management",
2653 "session_file_system",
2654 "session_storage",
2655 "session",
2656 "session_sql_database",
2657 "test_math",
2658 "test_weather",
2659 "stateless_todo_list",
2660 "web_fetch",
2661 "bashkit_shell",
2662 "background_execution",
2663 "session_schedule",
2664 "btw",
2665 "infinity_context",
2666 "compaction",
2667 "memory",
2668 "message_metadata",
2669 "openai_tool_search",
2670 "claude_tool_search",
2671 "tool_search",
2672 "auto_tool_search",
2673 "prompt_caching",
2674 "parallel_tool_calls",
2675 "session_tasks",
2676 "skills",
2677 "subagents",
2678 "system_commands",
2679 "sample_data",
2680 "data_knowledge",
2681 "knowledge_base",
2682 "knowledge_index",
2683 "tool_output_persistence",
2684 "tool_output_distillation",
2685 "fake_warehouse",
2686 "fake_aws",
2687 "fake_crm",
2688 "fake_financial",
2689 "loop_detection",
2690 "tool_call_repair",
2691 "error_disclosure",
2692 "prompt_canary_guardrail",
2693 "guardrails",
2694 "user_hooks",
2695 "model_scout",
2696 "openrouter_workspace",
2697 "openrouter_server_tools",
2698 ]
2699 .into_iter()
2700 .collect::<BTreeSet<_>>();
2701 if cfg!(feature = "ui-capabilities") {
2702 ids.insert("openui");
2703 ids.insert("a2ui");
2704 }
2705 ids
2706 }
2707
2708 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2710 let mut ids = expected_core_builtin_ids();
2711 ids.insert("agent_handoff");
2712 ids.insert("a2a_agent_delegation");
2713 ids
2714 }
2715
2716 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2717 registry.capabilities.keys().map(String::as_str).collect()
2718 }
2719
2720 #[test]
2730 fn test_capability_registry_with_builtins_dev() {
2731 let _lock = lock_env();
2733 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2734 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2735 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2736 assert!(registry.has("agent_handoff"));
2737 assert!(registry.has("a2a_agent_delegation"));
2738 }
2739
2740 #[test]
2741 fn test_capability_registry_with_builtins_prod() {
2742 let _lock = lock_env();
2744 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2745 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2746 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2747 assert!(!registry.has("docker_container"));
2749 assert!(!registry.has("agent_handoff"));
2750 assert!(!registry.has("a2a_agent_delegation"));
2751 }
2752
2753 #[test]
2754 fn test_agent_delegation_enabled_by_env_in_prod() {
2755 let _lock = lock_env();
2757 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2758 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2759 assert!(registry.has("agent_handoff"));
2760 assert!(registry.has("a2a_agent_delegation"));
2761 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2762 }
2763
2764 #[test]
2765 fn test_agent_delegation_disabled_by_env_in_dev() {
2766 let _lock = lock_env();
2768 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2769 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2770 assert!(!registry.has("agent_handoff"));
2771 assert!(!registry.has("a2a_agent_delegation"));
2772 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2773 }
2774
2775 #[test]
2776 fn test_capability_registry_get() {
2777 let registry = CapabilityRegistry::with_builtins();
2778
2779 let noop = registry.get("noop").unwrap();
2780 assert_eq!(noop.id(), "noop");
2781 assert_eq!(noop.name(), "No-Op");
2782 assert_eq!(noop.status(), CapabilityStatus::Available);
2783 }
2784
2785 #[test]
2793 fn builtin_capabilities_satisfy_registry_invariants() {
2794 let registry = CapabilityRegistry::with_builtins();
2795
2796 for cap in registry.list() {
2797 let id = cap.id();
2798 assert!(!id.is_empty(), "capability has an empty id");
2799 assert!(
2800 !cap.name().trim().is_empty(),
2801 "capability `{id}` has an empty name"
2802 );
2803
2804 assert!(
2807 registry.get(id).is_some(),
2808 "capability `{id}` does not resolve by its own id"
2809 );
2810
2811 for dep in cap.dependencies() {
2815 assert!(
2816 registry.get(dep).is_some(),
2817 "capability `{id}` depends on `{dep}`, which is not registered"
2818 );
2819 }
2820
2821 let mut seen = std::collections::HashSet::new();
2824 for tool in cap.tools() {
2825 let name = tool.name().to_string();
2826 assert!(
2827 !name.is_empty(),
2828 "capability `{id}` exposes a tool with an empty name"
2829 );
2830 assert!(
2831 seen.insert(name.clone()),
2832 "capability `{id}` exposes duplicate tool name `{name}`"
2833 );
2834 }
2835
2836 let mut def_seen = std::collections::HashSet::new();
2839 for def in cap.tool_definitions() {
2840 let name = def.name().to_string();
2841 assert!(
2842 !name.is_empty(),
2843 "capability `{id}` advertises a tool definition with an empty name"
2844 );
2845 assert!(
2846 def_seen.insert(name.clone()),
2847 "capability `{id}` advertises duplicate tool definition name `{name}`"
2848 );
2849 }
2850 }
2851 }
2852
2853 #[test]
2854 fn test_capability_registry_blueprint_with_capability() {
2855 struct BlueprintProviderCapability;
2856
2857 impl Capability for BlueprintProviderCapability {
2858 fn id(&self) -> &str {
2859 "blueprint_provider"
2860 }
2861 fn name(&self) -> &str {
2862 "Blueprint Provider"
2863 }
2864 fn description(&self) -> &str {
2865 "Capability that provides a blueprint for tests"
2866 }
2867 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2868 vec![AgentBlueprint {
2869 id: "test_blueprint",
2870 name: "Test Blueprint",
2871 description: "Blueprint for capability registry tests",
2872 model: BlueprintModel::Inherit,
2873 system_prompt: "Test prompt",
2874 tools: vec![],
2875 max_turns: None,
2876 config_schema: None,
2877 }]
2878 }
2879 }
2880
2881 let mut registry = CapabilityRegistry::new();
2882 registry.register(BlueprintProviderCapability);
2883
2884 let (capability_id, blueprint) = registry
2885 .blueprint_with_capability("test_blueprint")
2886 .expect("blueprint should resolve with capability id");
2887 assert_eq!(capability_id, "blueprint_provider");
2888 assert_eq!(blueprint.id, "test_blueprint");
2889 }
2890
2891 #[test]
2892 fn test_capability_registry_builder() {
2893 let registry = CapabilityRegistry::builder()
2894 .capability(NoopCapability)
2895 .capability(CurrentTimeCapability)
2896 .build();
2897
2898 assert!(registry.has("noop"));
2899 assert!(registry.has("current_time"));
2900 assert_eq!(registry.len(), 2);
2901 }
2902
2903 #[test]
2904 fn test_capability_status() {
2905 let registry = CapabilityRegistry::with_builtins();
2906
2907 let current_time = registry.get("current_time").unwrap();
2908 assert_eq!(current_time.status(), CapabilityStatus::Available);
2909
2910 let research = registry.get("research").unwrap();
2911 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2912 }
2913
2914 #[test]
2915 fn test_capability_icons_and_categories() {
2916 let registry = CapabilityRegistry::with_builtins();
2917
2918 let noop = registry.get("noop").unwrap();
2919 assert_eq!(noop.icon(), Some("circle-off"));
2920 assert_eq!(noop.category(), Some("Testing"));
2921
2922 let current_time = registry.get("current_time").unwrap();
2923 assert_eq!(current_time.icon(), Some("clock"));
2924 assert_eq!(current_time.category(), Some("Core"));
2925 }
2926
2927 #[test]
2928 fn test_system_prompt_preview_default_delegates_to_addition() {
2929 let registry = CapabilityRegistry::with_builtins();
2930
2931 let test_math = registry.get("test_math").unwrap();
2933 assert_eq!(
2934 test_math.system_prompt_preview().as_deref(),
2935 test_math.system_prompt_addition()
2936 );
2937
2938 let current_time = registry.get("current_time").unwrap();
2940 assert!(current_time.system_prompt_preview().is_none());
2941 assert!(current_time.system_prompt_addition().is_none());
2942 }
2943
2944 #[test]
2945 fn test_system_prompt_preview_dynamic_capability() {
2946 let registry = CapabilityRegistry::with_builtins();
2947 let cap = registry.get("agent_instructions").unwrap();
2948
2949 assert!(cap.system_prompt_addition().is_none());
2951 assert!(cap.system_prompt_preview().is_some());
2952 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2953 }
2954
2955 #[tokio::test]
2960 async fn test_apply_capabilities_empty() {
2961 let registry = CapabilityRegistry::with_builtins();
2962 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2963
2964 let applied =
2965 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
2966
2967 assert_eq!(
2968 applied.runtime_agent.system_prompt,
2969 base_runtime_agent.system_prompt
2970 );
2971 assert!(applied.tool_registry.is_empty());
2972 assert!(applied.applied_ids.is_empty());
2973 }
2974
2975 #[tokio::test]
2976 async fn test_apply_capabilities_noop() {
2977 let registry = CapabilityRegistry::with_builtins();
2978 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2979
2980 let applied = apply_capabilities(
2981 base_runtime_agent.clone(),
2982 &["noop".to_string()],
2983 ®istry,
2984 &test_ctx(),
2985 )
2986 .await;
2987
2988 assert_eq!(
2990 applied.runtime_agent.system_prompt,
2991 base_runtime_agent.system_prompt
2992 );
2993 assert!(applied.tool_registry.is_empty());
2994 assert_eq!(applied.applied_ids, vec!["noop"]);
2995 }
2996
2997 #[tokio::test]
2998 async fn test_apply_capabilities_current_time() {
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 &["current_time".to_string()],
3005 ®istry,
3006 &test_ctx(),
3007 )
3008 .await;
3009
3010 assert_eq!(
3012 applied.runtime_agent.system_prompt,
3013 base_runtime_agent.system_prompt
3014 );
3015 assert!(applied.tool_registry.has("get_current_time"));
3016 assert_eq!(applied.tool_registry.len(), 1);
3017 assert_eq!(applied.applied_ids, vec!["current_time"]);
3018 }
3019
3020 #[tokio::test]
3021 async fn test_apply_capabilities_skips_coming_soon() {
3022 let registry = CapabilityRegistry::with_builtins();
3023 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3024
3025 let applied = apply_capabilities(
3027 base_runtime_agent.clone(),
3028 &["research".to_string()],
3029 ®istry,
3030 &test_ctx(),
3031 )
3032 .await;
3033
3034 assert_eq!(
3036 applied.runtime_agent.system_prompt,
3037 base_runtime_agent.system_prompt
3038 );
3039 assert!(applied.applied_ids.is_empty()); }
3041
3042 #[tokio::test]
3043 async fn test_apply_capabilities_multiple() {
3044 let registry = CapabilityRegistry::with_builtins();
3045 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3046
3047 let applied = apply_capabilities(
3048 base_runtime_agent.clone(),
3049 &["noop".to_string(), "current_time".to_string()],
3050 ®istry,
3051 &test_ctx(),
3052 )
3053 .await;
3054
3055 assert!(applied.tool_registry.has("get_current_time"));
3056 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3057 }
3058
3059 #[tokio::test]
3060 async fn test_apply_capabilities_preserves_order() {
3061 let registry = CapabilityRegistry::with_builtins();
3062 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3063
3064 let applied = apply_capabilities(
3066 base_runtime_agent,
3067 &["current_time".to_string(), "noop".to_string()],
3068 ®istry,
3069 &test_ctx(),
3070 )
3071 .await;
3072
3073 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3074 }
3075
3076 #[tokio::test]
3077 async fn test_apply_capabilities_test_math() {
3078 let registry = CapabilityRegistry::with_builtins();
3079 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3080
3081 let applied = apply_capabilities(
3082 base_runtime_agent.clone(),
3083 &["test_math".to_string()],
3084 ®istry,
3085 &test_ctx(),
3086 )
3087 .await;
3088
3089 assert!(
3091 !applied
3092 .runtime_agent
3093 .system_prompt
3094 .contains("<capability id=\"test_math\">")
3095 );
3096 assert!(
3098 applied
3099 .runtime_agent
3100 .system_prompt
3101 .contains("You are a helpful assistant.")
3102 );
3103 assert!(applied.tool_registry.has("add"));
3104 assert!(applied.tool_registry.has("subtract"));
3105 assert!(applied.tool_registry.has("multiply"));
3106 assert!(applied.tool_registry.has("divide"));
3107 assert_eq!(applied.tool_registry.len(), 4);
3108 }
3109
3110 #[tokio::test]
3111 async fn test_apply_capabilities_test_weather() {
3112 let registry = CapabilityRegistry::with_builtins();
3113 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3114
3115 let applied = apply_capabilities(
3116 base_runtime_agent.clone(),
3117 &["test_weather".to_string()],
3118 ®istry,
3119 &test_ctx(),
3120 )
3121 .await;
3122
3123 assert!(
3125 !applied
3126 .runtime_agent
3127 .system_prompt
3128 .contains("<capability id=\"test_weather\">")
3129 );
3130 assert!(applied.tool_registry.has("get_weather"));
3131 assert!(applied.tool_registry.has("get_forecast"));
3132 assert_eq!(applied.tool_registry.len(), 2);
3133 }
3134
3135 #[tokio::test]
3136 async fn test_apply_capabilities_test_math_and_test_weather() {
3137 let registry = CapabilityRegistry::with_builtins();
3138 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3139
3140 let applied = apply_capabilities(
3141 base_runtime_agent.clone(),
3142 &["test_math".to_string(), "test_weather".to_string()],
3143 ®istry,
3144 &test_ctx(),
3145 )
3146 .await;
3147
3148 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3151 assert!(applied.tool_registry.has("get_weather"));
3152 }
3153
3154 #[tokio::test]
3155 async fn test_apply_capabilities_stateless_todo_list() {
3156 let registry = CapabilityRegistry::with_builtins();
3157 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3158
3159 let applied = apply_capabilities(
3160 base_runtime_agent.clone(),
3161 &["stateless_todo_list".to_string()],
3162 ®istry,
3163 &test_ctx(),
3164 )
3165 .await;
3166
3167 assert!(
3169 applied
3170 .runtime_agent
3171 .system_prompt
3172 .contains("Task Management")
3173 );
3174 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3175 assert!(applied.tool_registry.has("write_todos"));
3176 assert_eq!(applied.tool_registry.len(), 1);
3177 }
3178
3179 #[tokio::test]
3180 async fn test_apply_capabilities_web_fetch() {
3181 let registry = CapabilityRegistry::with_builtins();
3182 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3183
3184 let applied = apply_capabilities(
3185 base_runtime_agent.clone(),
3186 &["web_fetch".to_string()],
3187 ®istry,
3188 &test_ctx(),
3189 )
3190 .await;
3191
3192 assert!(
3194 applied
3195 .runtime_agent
3196 .system_prompt
3197 .contains(&base_runtime_agent.system_prompt)
3198 );
3199 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3200 assert!(applied.tool_registry.has("web_fetch"));
3201 assert_eq!(applied.tool_registry.len(), 1);
3202 }
3203
3204 #[tokio::test]
3209 async fn test_xml_tags_wrap_capability_prompts() {
3210 let registry = CapabilityRegistry::with_builtins();
3211 let collected =
3212 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3213 .await;
3214
3215 assert_eq!(collected.system_prompt_parts.len(), 1);
3216 let part = &collected.system_prompt_parts[0];
3217 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3218 assert!(part.ends_with("</capability>"));
3219 assert!(part.contains("Task Management"));
3220 }
3221
3222 #[tokio::test]
3223 async fn test_xml_tags_multiple_capabilities() {
3224 let registry = CapabilityRegistry::with_builtins();
3225 let collected = collect_capabilities(
3226 &[
3227 "stateless_todo_list".to_string(),
3228 "session_schedule".to_string(),
3229 ],
3230 ®istry,
3231 &test_ctx(),
3232 )
3233 .await;
3234
3235 assert_eq!(collected.system_prompt_parts.len(), 2);
3236 assert!(
3237 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3238 );
3239 assert!(
3240 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3241 );
3242
3243 let prefix = collected.system_prompt_prefix().unwrap();
3244 assert!(prefix.contains("</capability>\n\n<capability"));
3246 }
3247
3248 #[tokio::test]
3249 async fn test_xml_tags_system_prompt_wrapping() {
3250 let registry = CapabilityRegistry::with_builtins();
3251 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3252
3253 let applied = apply_capabilities(
3254 base,
3255 &["stateless_todo_list".to_string()],
3256 ®istry,
3257 &test_ctx(),
3258 )
3259 .await;
3260
3261 let prompt = &applied.runtime_agent.system_prompt;
3262 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3263 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3265 assert!(prompt.contains("</capability>"));
3266 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3268 }
3269
3270 #[tokio::test]
3271 async fn test_no_xml_wrapping_without_capabilities() {
3272 let registry = CapabilityRegistry::with_builtins();
3273 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3274
3275 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3276
3277 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3279 assert!(
3280 !applied
3281 .runtime_agent
3282 .system_prompt
3283 .contains("<system-prompt>")
3284 );
3285 }
3286
3287 #[tokio::test]
3288 async fn test_no_xml_wrapping_for_noop_capability() {
3289 let registry = CapabilityRegistry::with_builtins();
3290 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3291
3292 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3294
3295 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3296 assert!(
3297 !applied
3298 .runtime_agent
3299 .system_prompt
3300 .contains("<system-prompt>")
3301 );
3302 }
3303
3304 #[tokio::test]
3309 async fn test_collect_capabilities_includes_mounts() {
3310 let registry = CapabilityRegistry::with_builtins();
3311
3312 let collected =
3313 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3314
3315 assert!(!collected.mounts.is_empty());
3316 assert_eq!(collected.mounts.len(), 1);
3317 assert_eq!(collected.mounts[0].path, "/samples");
3318 assert!(collected.mounts[0].is_readonly());
3319 }
3320
3321 #[tokio::test]
3322 async fn test_collect_capabilities_empty_mounts_by_default() {
3323 let registry = CapabilityRegistry::with_builtins();
3324
3325 let collected =
3327 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3328
3329 assert!(collected.mounts.is_empty());
3330 }
3331
3332 #[tokio::test]
3333 async fn test_collect_capabilities_combines_mounts() {
3334 let registry = CapabilityRegistry::with_builtins();
3335
3336 let collected = collect_capabilities(
3339 &["sample_data".to_string(), "current_time".to_string()],
3340 ®istry,
3341 &test_ctx(),
3342 )
3343 .await;
3344
3345 assert_eq!(collected.mounts.len(), 1);
3346 assert!(
3348 collected
3349 .applied_ids
3350 .iter()
3351 .any(|id| id == "session_file_system")
3352 );
3353 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3354 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3355 }
3356
3357 #[test]
3358 fn test_sample_data_capability() {
3359 let registry = CapabilityRegistry::with_builtins();
3360 let cap = registry.get("sample_data").unwrap();
3361
3362 assert_eq!(cap.id(), "sample_data");
3363 assert_eq!(cap.name(), "Sample Data");
3364 assert_eq!(cap.status(), CapabilityStatus::Available);
3365
3366 assert!(cap.system_prompt_addition().is_some());
3368 assert!(cap.tools().is_empty());
3369
3370 assert!(!cap.mounts().is_empty());
3372 }
3373
3374 #[test]
3379 fn test_resolve_dependencies_empty() {
3380 let registry = CapabilityRegistry::with_builtins();
3381
3382 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3383
3384 assert!(resolved.resolved_ids.is_empty());
3385 assert!(resolved.added_as_dependencies.is_empty());
3386 assert!(resolved.user_selected.is_empty());
3387 }
3388
3389 #[test]
3390 fn test_resolve_dependencies_no_deps() {
3391 let registry = CapabilityRegistry::with_builtins();
3392
3393 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3395
3396 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3397 assert!(resolved.added_as_dependencies.is_empty());
3398 }
3399
3400 #[test]
3401 fn test_resolve_dependencies_with_deps() {
3402 let registry = CapabilityRegistry::with_builtins();
3403
3404 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3406
3407 assert_eq!(resolved.resolved_ids.len(), 2);
3409 let fs_pos = resolved
3410 .resolved_ids
3411 .iter()
3412 .position(|id| id == "session_file_system")
3413 .unwrap();
3414 let sd_pos = resolved
3415 .resolved_ids
3416 .iter()
3417 .position(|id| id == "sample_data")
3418 .unwrap();
3419 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3420
3421 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3423 }
3424
3425 #[test]
3426 fn test_resolve_dependencies_already_selected() {
3427 let registry = CapabilityRegistry::with_builtins();
3428
3429 let resolved = resolve_dependencies(
3431 &["session_file_system".to_string(), "sample_data".to_string()],
3432 ®istry,
3433 )
3434 .unwrap();
3435
3436 assert_eq!(resolved.resolved_ids.len(), 2);
3437 assert!(resolved.added_as_dependencies.is_empty());
3439 }
3440
3441 #[test]
3442 fn test_resolve_dependencies_preserves_order() {
3443 let registry = CapabilityRegistry::with_builtins();
3444
3445 let resolved =
3447 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
3448 .unwrap();
3449
3450 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
3451 }
3452
3453 #[test]
3454 fn test_resolve_dependencies_unknown_capability() {
3455 let registry = CapabilityRegistry::with_builtins();
3456
3457 let resolved =
3459 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
3460
3461 assert!(resolved.resolved_ids.is_empty());
3462 }
3463
3464 #[test]
3465 fn test_get_dependencies() {
3466 let registry = CapabilityRegistry::with_builtins();
3467
3468 let deps = get_dependencies("sample_data", ®istry);
3470 assert_eq!(deps, vec!["session_file_system"]);
3471
3472 let deps = get_dependencies("current_time", ®istry);
3474 assert!(deps.is_empty());
3475
3476 let deps = get_dependencies("unknown", ®istry);
3478 assert!(deps.is_empty());
3479 }
3480
3481 #[test]
3482 fn test_sample_data_has_dependency() {
3483 let registry = CapabilityRegistry::with_builtins();
3484 let cap = registry.get("sample_data").unwrap();
3485
3486 let deps = cap.dependencies();
3487 assert_eq!(deps.len(), 1);
3488 assert_eq!(deps[0], "session_file_system");
3489 }
3490
3491 #[test]
3492 fn test_noop_has_no_dependencies() {
3493 let registry = CapabilityRegistry::with_builtins();
3494 let cap = registry.get("noop").unwrap();
3495
3496 assert!(cap.dependencies().is_empty());
3497 }
3498
3499 #[test]
3503 fn test_circular_dependency_error() {
3504 struct CapA;
3506 struct CapB;
3507
3508 impl Capability for CapA {
3509 fn id(&self) -> &str {
3510 "test_cap_a"
3511 }
3512 fn name(&self) -> &str {
3513 "Test A"
3514 }
3515 fn description(&self) -> &str {
3516 "Test capability A"
3517 }
3518 fn dependencies(&self) -> Vec<&'static str> {
3519 vec!["test_cap_b"]
3520 }
3521 }
3522
3523 impl Capability for CapB {
3524 fn id(&self) -> &str {
3525 "test_cap_b"
3526 }
3527 fn name(&self) -> &str {
3528 "Test B"
3529 }
3530 fn description(&self) -> &str {
3531 "Test capability B"
3532 }
3533 fn dependencies(&self) -> Vec<&'static str> {
3534 vec!["test_cap_a"]
3535 }
3536 }
3537
3538 let mut registry = CapabilityRegistry::new();
3539 registry.register(CapA);
3540 registry.register(CapB);
3541
3542 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
3543
3544 assert!(result.is_err());
3545 match result.unwrap_err() {
3546 DependencyError::CircularDependency { capability_id, .. } => {
3547 assert_eq!(capability_id, "test_cap_a");
3548 }
3549 _ => panic!("Expected CircularDependency error"),
3550 }
3551 }
3552
3553 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
3558
3559 struct FilterTestCapability {
3561 priority: i32,
3562 }
3563
3564 impl Capability for FilterTestCapability {
3565 fn id(&self) -> &str {
3566 "filter_test"
3567 }
3568 fn name(&self) -> &str {
3569 "Filter Test"
3570 }
3571 fn description(&self) -> &str {
3572 "Test capability with message filter"
3573 }
3574 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3575 Some(Arc::new(FilterTestProvider {
3576 priority: self.priority,
3577 }))
3578 }
3579 }
3580
3581 struct FilterTestProvider {
3582 priority: i32,
3583 }
3584
3585 impl MessageFilterProvider for FilterTestProvider {
3586 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
3587 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
3589 query
3590 .filters
3591 .push(MessageFilter::Search(search.to_string()));
3592 }
3593 }
3594
3595 fn priority(&self) -> i32 {
3596 self.priority
3597 }
3598 }
3599
3600 #[tokio::test]
3601 async fn test_collect_capabilities_with_configs_no_filter_providers() {
3602 let registry = CapabilityRegistry::with_builtins();
3603 let configs = vec![AgentCapabilityConfig {
3604 capability_ref: CapabilityId::new("current_time"),
3605 config: serde_json::json!({}),
3606 }];
3607
3608 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3609
3610 assert!(collected.message_filter_providers.is_empty());
3611 assert!(!collected.has_message_filters());
3612 }
3613
3614 #[tokio::test]
3615 async fn test_collect_capabilities_with_configs_with_filter_provider() {
3616 let mut registry = CapabilityRegistry::new();
3617 registry.register(FilterTestCapability { priority: 0 });
3618
3619 let configs = vec![AgentCapabilityConfig {
3620 capability_ref: CapabilityId::new("filter_test"),
3621 config: serde_json::json!({ "search": "hello" }),
3622 }];
3623
3624 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3625
3626 assert_eq!(collected.message_filter_providers.len(), 1);
3627 assert!(collected.has_message_filters());
3628 }
3629
3630 #[tokio::test]
3631 async fn test_collect_capabilities_with_configs_filter_priority_order() {
3632 struct HighPriorityCapability;
3634 struct LowPriorityCapability;
3635
3636 impl Capability for HighPriorityCapability {
3637 fn id(&self) -> &str {
3638 "high_priority"
3639 }
3640 fn name(&self) -> &str {
3641 "High Priority"
3642 }
3643 fn description(&self) -> &str {
3644 "Test"
3645 }
3646 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3647 Some(Arc::new(FilterTestProvider { priority: 10 }))
3648 }
3649 }
3650
3651 impl Capability for LowPriorityCapability {
3652 fn id(&self) -> &str {
3653 "low_priority"
3654 }
3655 fn name(&self) -> &str {
3656 "Low Priority"
3657 }
3658 fn description(&self) -> &str {
3659 "Test"
3660 }
3661 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3662 Some(Arc::new(FilterTestProvider { priority: -5 }))
3663 }
3664 }
3665
3666 let mut registry = CapabilityRegistry::new();
3667 registry.register(HighPriorityCapability);
3668 registry.register(LowPriorityCapability);
3669
3670 let configs = vec![
3672 AgentCapabilityConfig {
3673 capability_ref: CapabilityId::new("high_priority"),
3674 config: serde_json::json!({}),
3675 },
3676 AgentCapabilityConfig {
3677 capability_ref: CapabilityId::new("low_priority"),
3678 config: serde_json::json!({}),
3679 },
3680 ];
3681
3682 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3683
3684 assert_eq!(collected.message_filter_providers.len(), 2);
3686 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
3687 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
3688 }
3689
3690 #[tokio::test]
3691 async fn test_collected_capabilities_apply_message_filters() {
3692 let mut registry = CapabilityRegistry::new();
3693 registry.register(FilterTestCapability { priority: 0 });
3694
3695 let configs = vec![AgentCapabilityConfig {
3696 capability_ref: CapabilityId::new("filter_test"),
3697 config: serde_json::json!({ "search": "test_query" }),
3698 }];
3699
3700 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3701
3702 let session_id: SessionId = Uuid::now_v7().into();
3704 let mut query = MessageQuery::new(session_id);
3705
3706 collected.apply_message_filters(&mut query);
3707
3708 assert_eq!(query.filters.len(), 1);
3710 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3711 }
3712
3713 #[tokio::test]
3714 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3715 struct SearchCapability {
3716 id: &'static str,
3717 search_term: &'static str,
3718 priority: i32,
3719 }
3720
3721 struct SearchProvider {
3722 search_term: &'static str,
3723 priority: i32,
3724 }
3725
3726 impl MessageFilterProvider for SearchProvider {
3727 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3728 query
3729 .filters
3730 .push(MessageFilter::Search(self.search_term.to_string()));
3731 }
3732
3733 fn priority(&self) -> i32 {
3734 self.priority
3735 }
3736 }
3737
3738 impl Capability for SearchCapability {
3739 fn id(&self) -> &str {
3740 self.id
3741 }
3742 fn name(&self) -> &str {
3743 "Search"
3744 }
3745 fn description(&self) -> &str {
3746 "Test"
3747 }
3748 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3749 Some(Arc::new(SearchProvider {
3750 search_term: self.search_term,
3751 priority: self.priority,
3752 }))
3753 }
3754 }
3755
3756 let mut registry = CapabilityRegistry::new();
3757 registry.register(SearchCapability {
3758 id: "cap_a",
3759 search_term: "alpha",
3760 priority: 5,
3761 });
3762 registry.register(SearchCapability {
3763 id: "cap_b",
3764 search_term: "beta",
3765 priority: 1,
3766 });
3767 registry.register(SearchCapability {
3768 id: "cap_c",
3769 search_term: "gamma",
3770 priority: 10,
3771 });
3772
3773 let configs = vec![
3774 AgentCapabilityConfig {
3775 capability_ref: CapabilityId::new("cap_a"),
3776 config: serde_json::json!({}),
3777 },
3778 AgentCapabilityConfig {
3779 capability_ref: CapabilityId::new("cap_b"),
3780 config: serde_json::json!({}),
3781 },
3782 AgentCapabilityConfig {
3783 capability_ref: CapabilityId::new("cap_c"),
3784 config: serde_json::json!({}),
3785 },
3786 ];
3787
3788 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3789
3790 let session_id: SessionId = Uuid::now_v7().into();
3791 let mut query = MessageQuery::new(session_id);
3792
3793 collected.apply_message_filters(&mut query);
3794
3795 assert_eq!(query.filters.len(), 3);
3797 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3798 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3799 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3800 }
3801
3802 #[test]
3803 fn test_capability_without_message_filter_returns_none() {
3804 let registry = CapabilityRegistry::with_builtins();
3805
3806 let noop = registry.get("noop").unwrap();
3807 assert!(noop.message_filter_provider().is_none());
3808
3809 let current_time = registry.get("current_time").unwrap();
3810 assert!(current_time.message_filter_provider().is_none());
3811 }
3812
3813 #[tokio::test]
3814 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3815 let mut registry = CapabilityRegistry::new();
3816 registry.register(FilterTestCapability { priority: 0 });
3817
3818 let test_config = serde_json::json!({
3819 "search": "custom_search",
3820 "extra_field": 42
3821 });
3822
3823 let configs = vec![AgentCapabilityConfig {
3824 capability_ref: CapabilityId::new("filter_test"),
3825 config: test_config.clone(),
3826 }];
3827
3828 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3829
3830 assert_eq!(collected.message_filter_providers.len(), 1);
3832 let (_, stored_config) = &collected.message_filter_providers[0];
3833 assert_eq!(*stored_config, test_config);
3834 }
3835
3836 #[test]
3841 fn test_collect_message_filters_only_collects_filters() {
3842 let mut registry = CapabilityRegistry::new();
3843 registry.register(FilterTestCapability { priority: 0 });
3844
3845 let configs = vec![AgentCapabilityConfig {
3846 capability_ref: CapabilityId::new("filter_test"),
3847 config: serde_json::json!({ "search": "test_query" }),
3848 }];
3849
3850 let collected = collect_message_filters_only(&configs, ®istry);
3851
3852 let session_id: SessionId = Uuid::now_v7().into();
3853 let mut query = MessageQuery::new(session_id);
3854 collected.apply_message_filters(&mut query);
3855
3856 assert_eq!(query.filters.len(), 1);
3857 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3858 }
3859
3860 #[test]
3861 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
3862 let base = serde_json::json!({ "context_budget_tokens": 1000 });
3863
3864 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
3866 assert_eq!(with["compaction_active"], serde_json::json!(true));
3867 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
3868
3869 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
3870 assert!(without.get("compaction_active").is_none());
3871
3872 let other = message_filter_config_for("other", &base, true);
3874 assert!(other.get("compaction_active").is_none());
3875
3876 let null_base = message_filter_config_for(
3878 INFINITY_CONTEXT_CAPABILITY_ID,
3879 &serde_json::Value::Null,
3880 true,
3881 );
3882 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
3883 }
3884
3885 #[test]
3886 fn test_infinity_context_defers_to_compaction_end_to_end() {
3887 use crate::message::Message;
3888
3889 let mut registry = CapabilityRegistry::new();
3890 registry.register(InfinityContextCapability);
3891 registry.register(CompactionCapability);
3892
3893 let tight = serde_json::json!({
3894 "context_budget_tokens": 1,
3895 "min_recent_messages": 1
3896 });
3897
3898 let solo = vec![AgentCapabilityConfig {
3900 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3901 config: tight.clone(),
3902 }];
3903 let mut messages = vec![
3904 Message::user("task"),
3905 Message::assistant("old ".repeat(400)),
3906 Message::user("recent"),
3907 ];
3908 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
3909 assert!(
3910 messages
3911 .iter()
3912 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
3913 "infinity context alone should trim and notice"
3914 );
3915
3916 let both = vec![
3918 AgentCapabilityConfig {
3919 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3920 config: tight,
3921 },
3922 AgentCapabilityConfig {
3923 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3924 config: serde_json::json!({}),
3925 },
3926 ];
3927 let mut messages = vec![
3928 Message::user("task"),
3929 Message::assistant("old ".repeat(400)),
3930 Message::user("recent"),
3931 ];
3932 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
3933 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
3934 assert!(
3935 messages
3936 .iter()
3937 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
3938 "no hidden-history notice when compaction is the active reducer"
3939 );
3940 }
3941
3942 #[test]
3943 fn test_compaction_is_enabled_detects_compaction() {
3944 let mut registry = CapabilityRegistry::new();
3945 registry.register(CompactionCapability);
3946
3947 let with_compaction = vec![AgentCapabilityConfig {
3948 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3949 config: serde_json::json!({}),
3950 }];
3951 assert!(compaction_is_enabled(&with_compaction, ®istry));
3952
3953 let without = vec![AgentCapabilityConfig {
3954 capability_ref: CapabilityId::new("current_time"),
3955 config: serde_json::json!({}),
3956 }];
3957 assert!(!compaction_is_enabled(&without, ®istry));
3958 }
3959
3960 #[test]
3961 fn test_collect_message_filters_only_skips_unknown_capabilities() {
3962 let registry = CapabilityRegistry::new();
3963
3964 let configs = vec![AgentCapabilityConfig {
3965 capability_ref: CapabilityId::new("nonexistent"),
3966 config: serde_json::json!({}),
3967 }];
3968
3969 let collected = collect_message_filters_only(&configs, ®istry);
3970 assert!(collected.message_filter_providers.is_empty());
3971 }
3972
3973 #[test]
3974 fn test_collect_message_filters_only_preserves_priority_order() {
3975 struct PriorityFilterCap {
3976 id: &'static str,
3977 search_term: &'static str,
3978 priority: i32,
3979 }
3980
3981 struct PriorityFilterProvider {
3982 search_term: &'static str,
3983 priority: i32,
3984 }
3985
3986 impl Capability for PriorityFilterCap {
3987 fn id(&self) -> &str {
3988 self.id
3989 }
3990 fn name(&self) -> &str {
3991 self.id
3992 }
3993 fn description(&self) -> &str {
3994 "priority test"
3995 }
3996 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3997 Some(Arc::new(PriorityFilterProvider {
3998 search_term: self.search_term,
3999 priority: self.priority,
4000 }))
4001 }
4002 }
4003
4004 impl MessageFilterProvider for PriorityFilterProvider {
4005 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4006 query
4007 .filters
4008 .push(MessageFilter::Search(self.search_term.to_string()));
4009 }
4010 fn priority(&self) -> i32 {
4011 self.priority
4012 }
4013 }
4014
4015 let mut registry = CapabilityRegistry::new();
4016 registry.register(PriorityFilterCap {
4017 id: "gamma",
4018 search_term: "gamma",
4019 priority: 10,
4020 });
4021 registry.register(PriorityFilterCap {
4022 id: "alpha",
4023 search_term: "alpha",
4024 priority: 5,
4025 });
4026 registry.register(PriorityFilterCap {
4027 id: "beta",
4028 search_term: "beta",
4029 priority: 1,
4030 });
4031
4032 let configs = vec![
4033 AgentCapabilityConfig {
4034 capability_ref: CapabilityId::new("gamma"),
4035 config: serde_json::json!({}),
4036 },
4037 AgentCapabilityConfig {
4038 capability_ref: CapabilityId::new("alpha"),
4039 config: serde_json::json!({}),
4040 },
4041 AgentCapabilityConfig {
4042 capability_ref: CapabilityId::new("beta"),
4043 config: serde_json::json!({}),
4044 },
4045 ];
4046
4047 let collected = collect_message_filters_only(&configs, ®istry);
4048
4049 let session_id: SessionId = Uuid::now_v7().into();
4050 let mut query = MessageQuery::new(session_id);
4051 collected.apply_message_filters(&mut query);
4052
4053 assert_eq!(query.filters.len(), 3);
4055 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4056 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4057 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4058 }
4059
4060 #[test]
4061 fn test_collect_message_filters_only_post_load_invoked() {
4062 use crate::message::Message;
4063
4064 struct PostLoadCap;
4065 struct PostLoadProvider;
4066
4067 impl Capability for PostLoadCap {
4068 fn id(&self) -> &str {
4069 "post_load_test"
4070 }
4071 fn name(&self) -> &str {
4072 "PostLoad Test"
4073 }
4074 fn description(&self) -> &str {
4075 "test"
4076 }
4077 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4078 Some(Arc::new(PostLoadProvider))
4079 }
4080 }
4081
4082 impl MessageFilterProvider for PostLoadProvider {
4083 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4084 fn priority(&self) -> i32 {
4085 0
4086 }
4087 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4088 messages.reverse();
4090 }
4091 }
4092
4093 let mut registry = CapabilityRegistry::new();
4094 registry.register(PostLoadCap);
4095
4096 let configs = vec![AgentCapabilityConfig {
4097 capability_ref: CapabilityId::new("post_load_test"),
4098 config: serde_json::json!({}),
4099 }];
4100
4101 let collected = collect_message_filters_only(&configs, ®istry);
4102
4103 let mut messages = vec![Message::user("first"), Message::user("second")];
4104 collected.apply_post_load_filters(&mut messages);
4105
4106 assert_eq!(messages[0].text(), Some("second"));
4108 assert_eq!(messages[1].text(), Some("first"));
4109 }
4110
4111 #[test]
4112 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4113 use crate::tool_types::ToolCall;
4114
4115 fn tool_heavy_messages() -> Vec<Message> {
4116 let mut messages = vec![Message::user("inspect files repeatedly")];
4117 for index in 0..9 {
4118 let call_id = format!("call_{index}");
4119 messages.push(Message::assistant_with_tools(
4120 "",
4121 vec![ToolCall {
4122 id: call_id.clone(),
4123 name: "read_file".to_string(),
4124 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4125 }],
4126 ));
4127 messages.push(Message::tool_result(
4128 call_id,
4129 Some(serde_json::json!({
4130 "path": "/workspace/src/lib.rs",
4131 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4132 "total_lines": 1000,
4133 "lines_shown": {"start": 1, "end": 1000},
4134 "truncated": false
4135 })),
4136 None,
4137 ));
4138 }
4139 messages
4140 }
4141
4142 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4143 messages[2]
4144 .tool_result_content()
4145 .and_then(|result| result.result.as_ref())
4146 .and_then(|result| result.get("masked"))
4147 .and_then(|masked| masked.as_bool())
4148 .unwrap_or(false)
4149 }
4150
4151 let mut registry = CapabilityRegistry::new();
4152 registry.register(CompactionCapability);
4153 let context = ModelViewContext {
4154 session_id: SessionId::new(),
4155 prior_usage: None,
4156 };
4157
4158 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4159 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4160 assert!(!first_tool_result_is_masked(&unmasked));
4161
4162 let compaction = collect_model_view_providers(
4163 &[AgentCapabilityConfig {
4164 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4165 config: serde_json::json!({}),
4166 }],
4167 ®istry,
4168 None,
4169 );
4170 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4171 assert!(first_tool_result_is_masked(&masked));
4172 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4173 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4174 }
4175
4176 struct DelegatingFilterCap {
4179 id: &'static str,
4180 inner: std::sync::Arc<InnerFilterCap>,
4181 }
4182 struct InnerFilterCap;
4183
4184 impl Capability for InnerFilterCap {
4185 fn id(&self) -> &str {
4186 "inner_filter"
4187 }
4188 fn name(&self) -> &str {
4189 "Inner Filter"
4190 }
4191 fn description(&self) -> &str {
4192 "inner"
4193 }
4194 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4195 Some(std::sync::Arc::new(SentinelFilter))
4196 }
4197 }
4198 struct SentinelFilter;
4199 impl MessageFilterProvider for SentinelFilter {
4200 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4201 }
4202 impl Capability for DelegatingFilterCap {
4203 fn id(&self) -> &str {
4204 self.id
4205 }
4206 fn name(&self) -> &str {
4207 "Delegating Filter"
4208 }
4209 fn description(&self) -> &str {
4210 "delegating"
4211 }
4212 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4213 None }
4215 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4216 Some(&*self.inner)
4217 }
4218 }
4219
4220 #[test]
4221 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4222 let inner = std::sync::Arc::new(InnerFilterCap);
4223 let outer = DelegatingFilterCap {
4224 id: "delegating_filter",
4225 inner: inner.clone(),
4226 };
4227
4228 let mut registry = CapabilityRegistry::new();
4229 registry.register(outer);
4230
4231 let configs = vec![AgentCapabilityConfig {
4232 capability_ref: CapabilityId::new("delegating_filter"),
4233 config: serde_json::json!({}),
4234 }];
4235
4236 let collected = collect_message_filters_only(&configs, ®istry);
4239 assert_eq!(
4240 collected.message_filter_providers.len(),
4241 1,
4242 "provider from resolved inner capability must be collected"
4243 );
4244 }
4245
4246 struct DelegatingMvpCap {
4247 id: &'static str,
4248 inner: std::sync::Arc<InnerMvpCap>,
4249 }
4250 struct InnerMvpCap;
4251
4252 impl Capability for InnerMvpCap {
4253 fn id(&self) -> &str {
4254 "inner_mvp"
4255 }
4256 fn name(&self) -> &str {
4257 "Inner MVP"
4258 }
4259 fn description(&self) -> &str {
4260 "inner"
4261 }
4262 fn model_view_provider(
4263 &self,
4264 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4265 struct NoopMvp;
4267 impl crate::capabilities::ModelViewProvider for NoopMvp {
4268 fn apply_model_view(
4269 &self,
4270 messages: Vec<Message>,
4271 _config: &serde_json::Value,
4272 _context: &ModelViewContext<'_>,
4273 ) -> Vec<Message> {
4274 messages
4275 }
4276 }
4277 Some(std::sync::Arc::new(NoopMvp))
4278 }
4279 }
4280 impl Capability for DelegatingMvpCap {
4281 fn id(&self) -> &str {
4282 self.id
4283 }
4284 fn name(&self) -> &str {
4285 "Delegating MVP"
4286 }
4287 fn description(&self) -> &str {
4288 "delegating"
4289 }
4290 fn model_view_provider(
4291 &self,
4292 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4293 None }
4295 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4296 Some(&*self.inner)
4297 }
4298 }
4299
4300 #[test]
4301 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4302 let inner = std::sync::Arc::new(InnerMvpCap);
4303 let outer = DelegatingMvpCap {
4304 id: "delegating_mvp",
4305 inner: inner.clone(),
4306 };
4307
4308 let mut registry = CapabilityRegistry::new();
4309 registry.register(outer);
4310
4311 let configs = vec![AgentCapabilityConfig {
4312 capability_ref: CapabilityId::new("delegating_mvp"),
4313 config: serde_json::json!({}),
4314 }];
4315
4316 let collected = collect_model_view_providers(&configs, ®istry, None);
4319 assert_eq!(
4320 collected.model_view_providers.len(),
4321 1,
4322 "provider from resolved inner capability must be collected"
4323 );
4324 }
4325
4326 #[tokio::test]
4336 async fn test_bashkit_shell_capability_produces_bash_tool() {
4337 let registry = CapabilityRegistry::with_builtins();
4338 let collected =
4339 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4340
4341 let tool_names: Vec<&str> = collected
4342 .tool_definitions
4343 .iter()
4344 .map(|t| t.name())
4345 .collect();
4346 assert!(
4347 tool_names.contains(&"bash"),
4348 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4349 tool_names
4350 );
4351 assert!(
4352 !collected.tools.is_empty(),
4353 "bashkit_shell must provide tool implementations"
4354 );
4355 }
4356
4357 #[tokio::test]
4358 async fn test_generic_harness_capability_set_produces_bash_tool() {
4359 let generic_harness_caps = vec![
4362 "session_file_system".to_string(),
4363 "bashkit_shell".to_string(),
4364 "web_fetch".to_string(),
4365 "session_storage".to_string(),
4366 "session".to_string(),
4367 "agent_instructions".to_string(),
4368 "skills".to_string(),
4369 "infinity_context".to_string(),
4370 "auto_tool_search".to_string(),
4371 ];
4372
4373 let registry = CapabilityRegistry::with_builtins();
4374 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4375
4376 let tool_names: Vec<&str> = collected
4377 .tool_definitions
4378 .iter()
4379 .map(|t| t.name())
4380 .collect();
4381 assert!(
4382 tool_names.contains(&"bash"),
4383 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4384 tool_names
4385 );
4386 }
4387
4388 #[tokio::test]
4389 async fn test_collect_capabilities_tool_count_matches_definitions() {
4390 let registry = CapabilityRegistry::with_builtins();
4393 let collected =
4394 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4395
4396 assert_eq!(
4397 collected.tools.len(),
4398 collected.tool_definitions.len(),
4399 "tool implementations ({}) must match tool definitions ({})",
4400 collected.tools.len(),
4401 collected.tool_definitions.len(),
4402 );
4403 }
4404
4405 #[tokio::test]
4409 async fn test_collect_capabilities_resolves_dependencies() {
4410 let registry = CapabilityRegistry::with_builtins();
4413 let collected =
4414 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4415
4416 assert!(
4418 collected
4419 .applied_ids
4420 .iter()
4421 .any(|id| id == "session_file_system"),
4422 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4423 collected.applied_ids
4424 );
4425
4426 let tool_names: Vec<&str> = collected
4427 .tool_definitions
4428 .iter()
4429 .map(|t| t.name())
4430 .collect();
4431
4432 assert!(
4434 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4435 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
4436 tool_names
4437 );
4438
4439 assert_eq!(
4441 collected.tools.len(),
4442 collected.tool_definitions.len(),
4443 "dependency-added tools must have implementations, not just definitions"
4444 );
4445 }
4446
4447 #[test]
4448 fn test_defaults_do_not_include_bash() {
4449 let registry = crate::ToolRegistry::with_defaults();
4452 assert!(
4453 !registry.has("bash"),
4454 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
4455 );
4456 }
4457
4458 #[tokio::test]
4465 async fn test_background_execution_auto_activates_with_bashkit_shell() {
4466 let registry = CapabilityRegistry::with_builtins();
4467 let collected =
4468 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4469
4470 let tool_names: Vec<&str> = collected
4471 .tool_definitions
4472 .iter()
4473 .map(|t| t.name())
4474 .collect();
4475 assert!(
4476 tool_names.contains(&"spawn_background"),
4477 "spawn_background must be auto-activated when bashkit_shell (a \
4478 background-capable tool) is in the agent's capability set; got: {:?}",
4479 tool_names
4480 );
4481 assert!(
4482 collected
4483 .applied_ids
4484 .iter()
4485 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4486 "background_execution must be in applied_ids when auto-activated; \
4487 got: {:?}",
4488 collected.applied_ids
4489 );
4490
4491 assert!(
4493 collected
4494 .tools
4495 .iter()
4496 .any(|t| t.name() == "spawn_background"),
4497 "spawn_background tool implementation must be present alongside the \
4498 definition (lockstep contract)"
4499 );
4500 }
4501
4502 #[tokio::test]
4505 async fn test_background_execution_does_not_auto_activate_without_hint() {
4506 let registry = CapabilityRegistry::with_builtins();
4507 let collected =
4509 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4510
4511 let tool_names: Vec<&str> = collected
4512 .tool_definitions
4513 .iter()
4514 .map(|t| t.name())
4515 .collect();
4516 assert!(
4517 !tool_names.contains(&"spawn_background"),
4518 "spawn_background must NOT be activated without a background-capable \
4519 tool; got: {:?}",
4520 tool_names
4521 );
4522 assert!(
4523 !collected
4524 .applied_ids
4525 .iter()
4526 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4527 "background_execution must not appear in applied_ids when no \
4528 background-capable tool is present; got: {:?}",
4529 collected.applied_ids
4530 );
4531 }
4532
4533 #[tokio::test]
4537 async fn test_background_execution_explicit_selection_is_idempotent() {
4538 let registry = CapabilityRegistry::with_builtins();
4539 let collected = collect_capabilities(
4540 &[
4541 "bashkit_shell".to_string(),
4542 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
4543 ],
4544 ®istry,
4545 &test_ctx(),
4546 )
4547 .await;
4548
4549 let spawn_background_count = collected
4550 .tool_definitions
4551 .iter()
4552 .filter(|t| t.name() == "spawn_background")
4553 .count();
4554 assert_eq!(
4555 spawn_background_count, 1,
4556 "spawn_background must appear exactly once even when \
4557 background_execution is selected explicitly alongside a \
4558 background-capable tool"
4559 );
4560 let applied_count = collected
4561 .applied_ids
4562 .iter()
4563 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
4564 .count();
4565 assert_eq!(
4566 applied_count, 1,
4567 "background_execution must appear exactly once in applied_ids"
4568 );
4569 }
4570
4571 #[test]
4576 fn test_defaults_do_not_include_spawn_background() {
4577 let registry = crate::ToolRegistry::with_defaults();
4578 assert!(
4579 !registry.has("spawn_background"),
4580 "with_defaults() must not include 'spawn_background' — it comes \
4581 from the background_execution capability (EVE-501)"
4582 );
4583 }
4584
4585 #[test]
4590 fn test_capability_features_default_empty() {
4591 let registry = CapabilityRegistry::with_builtins();
4592
4593 let noop = registry.get("noop").unwrap();
4595 assert!(noop.features().is_empty());
4596
4597 let current_time = registry.get("current_time").unwrap();
4598 assert!(current_time.features().is_empty());
4599 }
4600
4601 #[test]
4602 fn test_file_system_capability_features() {
4603 let registry = CapabilityRegistry::with_builtins();
4604
4605 let fs = registry.get("session_file_system").unwrap();
4606 assert_eq!(fs.features(), vec!["file_system"]);
4607 }
4608
4609 #[test]
4610 fn test_bashkit_shell_capability_features() {
4611 let registry = CapabilityRegistry::with_builtins();
4612
4613 let bash = registry.get("bashkit_shell").unwrap();
4614 assert_eq!(bash.features(), vec!["file_system"]);
4615 }
4616
4617 #[test]
4618 fn test_alias_resolves_to_canonical_capability() {
4619 let registry = CapabilityRegistry::with_builtins();
4620
4621 let via_alias = registry.get("virtual_bash").unwrap();
4623 assert_eq!(via_alias.id(), "bashkit_shell");
4624 assert!(registry.has("virtual_bash"));
4625 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
4626 assert_eq!(
4627 registry.canonical_id("bashkit_shell"),
4628 Some("bashkit_shell")
4629 );
4630 assert_eq!(registry.canonical_id("nonexistent"), None);
4631 }
4632
4633 #[test]
4634 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
4635 let registry = CapabilityRegistry::with_builtins();
4636
4637 let resolved = resolve_dependencies(
4640 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
4641 ®istry,
4642 )
4643 .unwrap();
4644 let bash_ids: Vec<_> = resolved
4645 .resolved_ids
4646 .iter()
4647 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
4648 .collect();
4649 assert_eq!(bash_ids, vec!["bashkit_shell"]);
4650 assert!(
4652 !resolved
4653 .added_as_dependencies
4654 .contains(&"bashkit_shell".to_string())
4655 );
4656 }
4657
4658 #[test]
4659 fn test_alias_preserves_explicit_config_in_resolution() {
4660 let registry = CapabilityRegistry::with_builtins();
4661
4662 let configs = vec![AgentCapabilityConfig::with_config(
4663 "virtual_bash".to_string(),
4664 serde_json::json!({"key": "value"}),
4665 )];
4666 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
4667 let bash = resolved
4668 .iter()
4669 .find(|c| c.capability_id() == "bashkit_shell")
4670 .expect("alias must resolve to canonical bashkit_shell config");
4671 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
4672 }
4673
4674 #[test]
4675 fn test_unregister_by_alias_removes_capability_and_aliases() {
4676 let mut registry = CapabilityRegistry::with_builtins();
4677
4678 assert!(registry.unregister("virtual_bash").is_some());
4679 assert!(!registry.has("bashkit_shell"));
4680 assert!(!registry.has("virtual_bash"));
4681 }
4682
4683 #[test]
4684 fn test_session_storage_capability_features() {
4685 let registry = CapabilityRegistry::with_builtins();
4686
4687 let storage = registry.get("session_storage").unwrap();
4688 let features = storage.features();
4689 assert!(features.contains(&"secrets"));
4690 assert!(features.contains(&"key_value"));
4691 }
4692
4693 #[test]
4694 fn test_session_schedule_capability_features() {
4695 let registry = CapabilityRegistry::with_builtins();
4696
4697 let schedule = registry.get("session_schedule").unwrap();
4698 assert_eq!(schedule.features(), vec!["schedules"]);
4699 }
4700
4701 #[test]
4702 fn test_session_sql_database_capability_features() {
4703 let registry = CapabilityRegistry::with_builtins();
4704
4705 let sql = registry.get("session_sql_database").unwrap();
4706 assert_eq!(sql.features(), vec!["sql_database"]);
4707 }
4708
4709 #[test]
4710 fn test_sample_data_capability_features() {
4711 let registry = CapabilityRegistry::with_builtins();
4712
4713 let sample = registry.get("sample_data").unwrap();
4714 assert_eq!(sample.features(), vec!["file_system"]);
4715 }
4716
4717 #[test]
4718 fn test_compute_features_empty() {
4719 let registry = CapabilityRegistry::with_builtins();
4720
4721 let features = compute_features(&[], ®istry);
4722 assert!(features.is_empty());
4723 }
4724
4725 #[test]
4726 fn test_compute_features_single_capability() {
4727 let registry = CapabilityRegistry::with_builtins();
4728
4729 let features = compute_features(&["session_schedule".to_string()], ®istry);
4730 assert_eq!(features, vec!["schedules"]);
4731 }
4732
4733 #[test]
4734 fn test_compute_features_multiple_capabilities() {
4735 let registry = CapabilityRegistry::with_builtins();
4736
4737 let features = compute_features(
4738 &[
4739 "session_file_system".to_string(),
4740 "session_storage".to_string(),
4741 "session_schedule".to_string(),
4742 ],
4743 ®istry,
4744 );
4745 assert!(features.contains(&"file_system".to_string()));
4746 assert!(features.contains(&"secrets".to_string()));
4747 assert!(features.contains(&"key_value".to_string()));
4748 assert!(features.contains(&"schedules".to_string()));
4749 }
4750
4751 #[test]
4752 fn test_compute_features_deduplicates() {
4753 let registry = CapabilityRegistry::with_builtins();
4754
4755 let features = compute_features(
4757 &[
4758 "session_file_system".to_string(),
4759 "bashkit_shell".to_string(),
4760 ],
4761 ®istry,
4762 );
4763 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
4764 assert_eq!(file_system_count, 1, "file_system should appear only once");
4765 }
4766
4767 #[test]
4768 fn test_compute_features_includes_dependency_features() {
4769 let registry = CapabilityRegistry::with_builtins();
4770
4771 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
4773 assert!(features.contains(&"file_system".to_string()));
4774 }
4775
4776 #[test]
4777 fn test_compute_features_generic_harness_set() {
4778 let registry = CapabilityRegistry::with_builtins();
4779
4780 let features = compute_features(
4782 &[
4783 "session_file_system".to_string(),
4784 "bashkit_shell".to_string(),
4785 "session_storage".to_string(),
4786 "session".to_string(),
4787 "session_schedule".to_string(),
4788 ],
4789 ®istry,
4790 );
4791 assert!(features.contains(&"file_system".to_string()));
4792 assert!(features.contains(&"secrets".to_string()));
4793 assert!(features.contains(&"key_value".to_string()));
4794 assert!(features.contains(&"schedules".to_string()));
4795 }
4796
4797 #[test]
4798 fn test_compute_features_unknown_capability_ignored() {
4799 let registry = CapabilityRegistry::with_builtins();
4800
4801 let features = compute_features(
4802 &["unknown_cap".to_string(), "session_schedule".to_string()],
4803 ®istry,
4804 );
4805 assert_eq!(features, vec!["schedules"]);
4806 }
4807
4808 #[test]
4809 fn test_risk_level_ordering() {
4810 assert!(RiskLevel::Low < RiskLevel::Medium);
4811 assert!(RiskLevel::Medium < RiskLevel::High);
4812 }
4813
4814 #[test]
4815 fn test_risk_level_serde_roundtrip() {
4816 let high = RiskLevel::High;
4817 let json = serde_json::to_string(&high).unwrap();
4818 assert_eq!(json, "\"high\"");
4819 let back: RiskLevel = serde_json::from_str(&json).unwrap();
4820 assert_eq!(back, RiskLevel::High);
4821 }
4822
4823 #[test]
4824 fn test_capability_risk_levels() {
4825 let registry = CapabilityRegistry::with_builtins();
4826
4827 let bash = registry.get("bashkit_shell").unwrap();
4829 assert_eq!(bash.risk_level(), RiskLevel::High);
4830
4831 let fetch = registry.get("web_fetch").unwrap();
4833 assert_eq!(fetch.risk_level(), RiskLevel::High);
4834
4835 let noop = registry.get("noop").unwrap();
4837 assert_eq!(noop.risk_level(), RiskLevel::Low);
4838 }
4839
4840 #[tokio::test]
4845 async fn test_apply_capabilities_openai_tool_search() {
4846 let registry = CapabilityRegistry::with_builtins();
4847 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4848
4849 let applied = apply_capabilities(
4850 base_runtime_agent.clone(),
4851 &["openai_tool_search".to_string()],
4852 ®istry,
4853 &test_ctx(),
4854 )
4855 .await;
4856
4857 assert_eq!(
4859 applied.runtime_agent.system_prompt,
4860 base_runtime_agent.system_prompt
4861 );
4862 assert!(applied.tool_registry.is_empty());
4863 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
4864
4865 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4867 assert!(ts.enabled);
4868 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4869 }
4870
4871 #[tokio::test]
4872 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
4873 let registry = CapabilityRegistry::with_builtins();
4874 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4875
4876 let applied = apply_capabilities(
4877 base_runtime_agent,
4878 &[
4879 "current_time".to_string(),
4880 "openai_tool_search".to_string(),
4881 "test_math".to_string(),
4882 ],
4883 ®istry,
4884 &test_ctx(),
4885 )
4886 .await;
4887
4888 assert!(applied.tool_registry.has("get_current_time"));
4890 assert!(applied.tool_registry.has("add"));
4891 assert!(applied.tool_registry.has("subtract"));
4892 assert!(applied.tool_registry.has("multiply"));
4893 assert!(applied.tool_registry.has("divide"));
4894
4895 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4897 assert!(ts.enabled);
4898 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4899 }
4900
4901 #[tokio::test]
4902 async fn test_collect_capabilities_tool_search_custom_threshold() {
4903 let registry = CapabilityRegistry::with_builtins();
4904
4905 let configs = vec![AgentCapabilityConfig {
4906 capability_ref: CapabilityId::new("openai_tool_search"),
4907 config: serde_json::json!({"threshold": 5}),
4908 }];
4909
4910 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4911
4912 let ts = collected.tool_search.as_ref().unwrap();
4913 assert!(ts.enabled);
4914 assert_eq!(ts.threshold, 5);
4915 }
4916
4917 #[tokio::test]
4918 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
4919 let registry = CapabilityRegistry::with_builtins();
4920
4921 let configs = vec![
4922 AgentCapabilityConfig {
4923 capability_ref: CapabilityId::new("auto_tool_search"),
4924 config: serde_json::json!({"threshold": 2}),
4925 },
4926 AgentCapabilityConfig {
4927 capability_ref: CapabilityId::new("test_math"),
4928 config: serde_json::json!({}),
4929 },
4930 ];
4931
4932 let ctx = test_ctx().with_model("claude-3-5-haiku");
4936 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4937
4938 assert!(
4939 collected.tool_search.is_none(),
4940 "auto_tool_search must not set a hosted config on a non-native model"
4941 );
4942 assert!(
4943 collected
4944 .tools
4945 .iter()
4946 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4947 "auto_tool_search must contribute the client-side tool_search tool"
4948 );
4949 assert!(
4950 !collected.tool_definition_hooks.is_empty(),
4951 "auto_tool_search must contribute a client-side deferral hook"
4952 );
4953
4954 let mut transformed = collected.tool_definitions.clone();
4955 for hook in &collected.tool_definition_hooks {
4956 transformed = hook.transform(transformed);
4957 }
4958 let add_tool = transformed
4959 .iter()
4960 .find(|tool| tool.name() == "add")
4961 .expect("test_math contributes add");
4962 assert!(
4963 add_tool.parameters().get("properties").is_none(),
4964 "generic auto_tool_search must honor the configured threshold"
4965 );
4966 }
4967
4968 #[tokio::test]
4969 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
4970 let registry = CapabilityRegistry::with_builtins();
4971
4972 let configs = vec![AgentCapabilityConfig {
4973 capability_ref: CapabilityId::new("auto_tool_search"),
4974 config: serde_json::json!({"threshold": 7}),
4975 }];
4976
4977 let ctx = test_ctx().with_model("gpt-5.4");
4980 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4981
4982 let ts = collected
4983 .tool_search
4984 .as_ref()
4985 .expect("auto_tool_search must set a hosted config on a native model");
4986 assert!(ts.enabled);
4987 assert_eq!(ts.threshold, 7);
4988 assert!(
4989 !collected
4990 .tools
4991 .iter()
4992 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4993 "hosted mechanism must not contribute the client-side tool_search tool"
4994 );
4995 assert!(
4996 collected.tool_definition_hooks.is_empty(),
4997 "hosted mechanism must not contribute a client-side deferral hook"
4998 );
4999 }
5000
5001 #[tokio::test]
5002 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
5003 let registry = CapabilityRegistry::with_builtins();
5004
5005 let configs = vec![AgentCapabilityConfig {
5006 capability_ref: CapabilityId::new("auto_tool_search"),
5007 config: serde_json::json!({"threshold": 9}),
5008 }];
5009
5010 let ctx = test_ctx().with_model("claude-opus-4-8");
5013 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5014
5015 let ts = collected
5016 .tool_search
5017 .as_ref()
5018 .expect("auto_tool_search must set a hosted config on a native Claude model");
5019 assert!(ts.enabled);
5020 assert_eq!(ts.threshold, 9);
5021 assert!(
5022 !collected
5023 .tools
5024 .iter()
5025 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5026 "hosted mechanism must not contribute the client-side tool_search tool"
5027 );
5028 assert!(
5029 collected.tool_definition_hooks.is_empty(),
5030 "hosted mechanism must not contribute a client-side deferral hook"
5031 );
5032 }
5033
5034 #[tokio::test]
5035 async fn test_collect_capabilities_no_tool_search_without_capability() {
5036 let registry = CapabilityRegistry::with_builtins();
5037
5038 let configs = vec![AgentCapabilityConfig {
5039 capability_ref: CapabilityId::new("current_time"),
5040 config: serde_json::json!({}),
5041 }];
5042
5043 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5044
5045 assert!(collected.tool_search.is_none());
5046 }
5047
5048 #[tokio::test]
5049 async fn test_collect_capabilities_tool_search_category_propagation() {
5050 let registry = CapabilityRegistry::with_builtins();
5051
5052 let configs = vec![
5054 AgentCapabilityConfig {
5055 capability_ref: CapabilityId::new("test_math"),
5056 config: serde_json::json!({}),
5057 },
5058 AgentCapabilityConfig {
5059 capability_ref: CapabilityId::new("openai_tool_search"),
5060 config: serde_json::json!({}),
5061 },
5062 ];
5063
5064 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5065
5066 assert!(collected.tool_search.is_some());
5068
5069 for tool_def in &collected.tool_definitions {
5071 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
5073 assert!(
5074 tool_def.category().is_some(),
5075 "Tool {} should have a category from its capability",
5076 tool_def.name()
5077 );
5078 }
5079 }
5080 }
5081
5082 #[tokio::test]
5083 async fn test_apply_capabilities_prompt_caching() {
5084 let registry = CapabilityRegistry::with_builtins();
5085 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5086
5087 let applied = apply_capabilities(
5088 base_runtime_agent.clone(),
5089 &["prompt_caching".to_string()],
5090 ®istry,
5091 &test_ctx(),
5092 )
5093 .await;
5094
5095 assert_eq!(
5096 applied.runtime_agent.system_prompt,
5097 base_runtime_agent.system_prompt
5098 );
5099 assert!(applied.tool_registry.is_empty());
5100 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
5101
5102 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
5103 assert!(prompt_cache.enabled);
5104 assert_eq!(
5105 prompt_cache.strategy,
5106 crate::driver_registry::PromptCacheStrategy::Auto
5107 );
5108 assert!(prompt_cache.gemini_cached_content.is_none());
5109 }
5110
5111 #[tokio::test]
5112 async fn test_apply_capabilities_openrouter_server_tools() {
5113 let registry = CapabilityRegistry::with_builtins();
5114 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5115
5116 let configs = vec![AgentCapabilityConfig {
5117 capability_ref: CapabilityId::new("openrouter_server_tools"),
5118 config: serde_json::json!({
5119 "tools": ["web_search", "datetime"],
5120 "web_search_max_results": 4,
5121 }),
5122 }];
5123
5124 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5125 let routing = collected
5126 .openrouter_routing
5127 .as_ref()
5128 .expect("server tools produce routing config");
5129 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
5130 assert_eq!(
5131 kinds,
5132 vec![
5133 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
5134 crate::driver_registry::OpenRouterServerToolKind::Datetime,
5135 ]
5136 );
5137
5138 let applied = apply_capabilities(
5141 base_runtime_agent,
5142 &["openrouter_server_tools".to_string()],
5143 ®istry,
5144 &test_ctx(),
5145 )
5146 .await;
5147 assert!(applied.tool_registry.is_empty());
5148 assert!(applied.runtime_agent.openrouter_routing.is_none());
5149 }
5150
5151 #[tokio::test]
5152 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
5153 let registry = CapabilityRegistry::with_builtins();
5154
5155 let configs = vec![AgentCapabilityConfig {
5156 capability_ref: CapabilityId::new("prompt_caching"),
5157 config: serde_json::json!({"strategy": "auto"}),
5158 }];
5159
5160 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5161
5162 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5163 assert!(prompt_cache.enabled);
5164 assert_eq!(
5165 prompt_cache.strategy,
5166 crate::driver_registry::PromptCacheStrategy::Auto
5167 );
5168 assert!(prompt_cache.gemini_cached_content.is_none());
5169 }
5170
5171 #[tokio::test]
5172 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
5173 let registry = CapabilityRegistry::with_builtins();
5174
5175 let configs = vec![AgentCapabilityConfig {
5176 capability_ref: CapabilityId::new("prompt_caching"),
5177 config: serde_json::json!({
5178 "strategy": "auto",
5179 "gemini_cached_content": "cachedContents/demo-cache"
5180 }),
5181 }];
5182
5183 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5184
5185 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5186 assert_eq!(
5187 prompt_cache.gemini_cached_content.as_deref(),
5188 Some("cachedContents/demo-cache")
5189 );
5190 }
5191
5192 #[tokio::test]
5193 async fn test_collect_capabilities_parallel_tool_calls_modes() {
5194 let registry = CapabilityRegistry::with_builtins();
5195
5196 let collected = collect_capabilities_with_configs(
5198 &[AgentCapabilityConfig::new("parallel_tool_calls")],
5199 ®istry,
5200 &test_ctx(),
5201 )
5202 .await;
5203 assert_eq!(collected.parallel_tool_calls, Some(true));
5204
5205 let collected = collect_capabilities_with_configs(
5207 &[AgentCapabilityConfig {
5208 capability_ref: CapabilityId::new("parallel_tool_calls"),
5209 config: serde_json::json!({"mode": "avoid"}),
5210 }],
5211 ®istry,
5212 &test_ctx(),
5213 )
5214 .await;
5215 assert_eq!(collected.parallel_tool_calls, Some(false));
5216
5217 let collected = collect_capabilities_with_configs(
5219 &[AgentCapabilityConfig {
5220 capability_ref: CapabilityId::new("parallel_tool_calls"),
5221 config: serde_json::json!({"mode": "none"}),
5222 }],
5223 ®istry,
5224 &test_ctx(),
5225 )
5226 .await;
5227 assert_eq!(collected.parallel_tool_calls, None);
5228
5229 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
5231 assert_eq!(collected.parallel_tool_calls, None);
5232 }
5233
5234 #[tokio::test]
5235 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
5236 let registry = CapabilityRegistry::with_builtins();
5237
5238 let applied = apply_capabilities(
5240 RuntimeAgent::new("p", "gpt-5.2"),
5241 &["parallel_tool_calls".to_string()],
5242 ®istry,
5243 &test_ctx(),
5244 )
5245 .await;
5246 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
5247
5248 let mut base = RuntimeAgent::new("p", "gpt-5.2");
5250 base.parallel_tool_calls = Some(false);
5251 let applied = apply_capabilities(
5252 base,
5253 &["parallel_tool_calls".to_string()],
5254 ®istry,
5255 &test_ctx(),
5256 )
5257 .await;
5258 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
5259 }
5260
5261 struct SkillContributingCapability;
5266
5267 impl Capability for SkillContributingCapability {
5268 fn id(&self) -> &str {
5269 "contributes_skills"
5270 }
5271 fn name(&self) -> &str {
5272 "Contributes Skills"
5273 }
5274 fn description(&self) -> &str {
5275 "Test capability that contributes skills."
5276 }
5277 fn contribute_skills(&self) -> Vec<SkillContribution> {
5278 vec![
5279 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
5280 .with_files(vec![(
5281 "scripts/a.sh".to_string(),
5282 "#!/bin/sh\necho a\n".to_string(),
5283 )]),
5284 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
5285 .with_user_invocable(false),
5286 ]
5287 }
5288 }
5289
5290 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
5291 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
5292 MountSource::InlineFile { content, .. } => content.as_str(),
5293 _ => panic!("Expected InlineFile for SKILL.md"),
5294 }
5295 }
5296
5297 #[tokio::test]
5298 async fn test_contribute_skills_normalized_to_mounts() {
5299 let mut registry = CapabilityRegistry::new();
5300 registry.register(SkillContributingCapability);
5301
5302 let configs = vec![AgentCapabilityConfig {
5303 capability_ref: CapabilityId::new("contributes_skills"),
5304 config: serde_json::json!({}),
5305 }];
5306
5307 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5308
5309 let skill_mounts: Vec<_> = collected
5310 .mounts
5311 .iter()
5312 .filter(|m| m.path.starts_with("/.agents/skills/"))
5313 .collect();
5314 assert_eq!(skill_mounts.len(), 2);
5315
5316 for m in &skill_mounts {
5319 assert!(m.is_readonly());
5320 assert_eq!(m.capability_id, "contributes_skills");
5321 }
5322
5323 let alpha = skill_mounts
5324 .iter()
5325 .find(|m| m.path == "/.agents/skills/alpha-skill")
5326 .expect("alpha-skill mount missing");
5327 match &alpha.source {
5328 MountSource::InlineDirectory { entries } => {
5329 assert!(entries.contains_key("SKILL.md"));
5330 assert!(entries.contains_key("scripts/a.sh"));
5331 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5332 assert_eq!(parsed.name, "alpha-skill");
5333 assert!(parsed.user_invocable);
5334 }
5335 _ => panic!("Expected InlineDirectory"),
5336 }
5337
5338 let beta = skill_mounts
5339 .iter()
5340 .find(|m| m.path == "/.agents/skills/beta-skill")
5341 .expect("beta-skill mount missing");
5342 match &beta.source {
5343 MountSource::InlineDirectory { entries } => {
5344 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5345 assert!(!parsed.user_invocable);
5346 }
5347 _ => panic!("Expected InlineDirectory"),
5348 }
5349 }
5350
5351 #[tokio::test]
5352 async fn test_contribute_skills_default_empty() {
5353 let mut registry = CapabilityRegistry::new();
5356 registry.register(FilterTestCapability { priority: 0 });
5357
5358 let configs = vec![AgentCapabilityConfig {
5359 capability_ref: CapabilityId::new("filter_test"),
5360 config: serde_json::json!({}),
5361 }];
5362
5363 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5364 assert!(
5365 collected
5366 .mounts
5367 .iter()
5368 .all(|m| !m.path.starts_with("/.agents/skills/"))
5369 );
5370 }
5371
5372 struct LocalizedCapability;
5373
5374 impl Capability for LocalizedCapability {
5375 fn id(&self) -> &str {
5376 "localized"
5377 }
5378 fn name(&self) -> &str {
5379 "Localized"
5380 }
5381 fn description(&self) -> &str {
5382 "English description"
5383 }
5384 fn localizations(&self) -> Vec<CapabilityLocalization> {
5385 vec![
5386 CapabilityLocalization {
5387 locale: "en",
5388 name: None,
5389 description: None,
5390 config_description: Some("Controls things."),
5391 config_overlay: None,
5392 },
5393 CapabilityLocalization {
5394 locale: "uk",
5395 name: Some("Локалізована"),
5396 description: Some("Український опис"),
5397 config_description: Some("Керує налаштуваннями."),
5398 config_overlay: None,
5399 },
5400 ]
5401 }
5402 }
5403
5404 #[test]
5405 fn localized_name_falls_back_exact_language_then_base() {
5406 let cap = LocalizedCapability;
5407 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
5409 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
5410 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
5412 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
5414 assert_eq!(cap.localized_name(None), "Localized");
5415 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
5416 assert_eq!(cap.localized_description(Some("de")), "English description");
5417 }
5418
5419 #[test]
5420 fn describe_schema_resolves_config_description_per_locale() {
5421 let cap = LocalizedCapability;
5422 assert_eq!(
5423 cap.describe_schema(Some("uk-UA")).as_deref(),
5424 Some("Керує налаштуваннями.")
5425 );
5426 assert_eq!(
5428 cap.describe_schema(Some("pl")).as_deref(),
5429 Some("Controls things.")
5430 );
5431 assert_eq!(
5432 cap.describe_schema(None).as_deref(),
5433 Some("Controls things.")
5434 );
5435 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
5437 }
5438}