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;
152#[cfg(feature = "web-fetch")]
153mod web_fetch;
154
155pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
160pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
164#[cfg(feature = "a2a")]
165pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
166#[cfg(feature = "ui-capabilities")]
167pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
168pub use agent_handoff::{
169 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
170 MessageAgentHandoffTool, StartAgentHandoffTool,
171};
172pub use agent_instructions::{
173 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
174 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
175 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
176};
177pub use attach_skill::{
178 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
179 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
180 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
181};
182pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
183pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
184pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
185pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
186pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
187pub use compaction::{
188 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
189 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
190 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
191 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
192 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
193 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
194 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
195};
196pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
197pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
198pub use declarative::{
199 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
200 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
201 declarative_capability_info, hydrate_declarative_capability_config,
202 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
203 plugin_capability_info, validate_declarative_capability_definition,
204};
205pub use error_disclosure::{
206 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
207};
208pub use fake_aws::{
209 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
210 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
211 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
212 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
213};
214pub use fake_crm::{
215 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
216 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
217 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
218};
219pub use fake_financial::{
220 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
221 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
222 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
223 FinanceListTransactionsTool,
224};
225pub use fake_warehouse::{
226 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
227 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
228 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
229 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
230};
231pub use file_system::{
232 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
233 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
234};
235pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
236pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
237pub use infinity_context::{
238 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
239};
240pub use knowledge_base::{
241 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
242 validate_knowledge_base_config,
243};
244pub use knowledge_index::{
245 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
246 validate_knowledge_index_config,
247};
248pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
249pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
250pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
251pub use mcp::{
252 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
253 parse_mcp_capability_id,
254};
255pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
256pub use message_metadata::{
257 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
258 MessageMetadataField, render_annotation,
259};
260pub use model_scout::{
261 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
262 RouterUpdateProposal, compute_score, rank_results,
263};
264pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
265pub use openai_tool_search::{
266 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
267 model_supports_native_tool_search,
268};
269pub use openrouter_server_tools::{
270 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
271};
272pub use openrouter_workspace::{
273 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
274 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
275 detect_policy_drift,
276};
277#[cfg(feature = "ui-capabilities")]
278pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
279pub use parallel_tool_calls::{
280 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
281 parallel_tool_calls_from_config,
282};
283pub use platform_management::{
284 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
285 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
286 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
287};
288pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
289pub use prompt_canary_guardrail::{
290 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
291 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
292 REASON_CODE_SYSTEM_PROMPT_LEAK,
293};
294pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
295pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
296pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
297pub use session::{
298 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
299};
300pub use session_sandbox::{
301 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
302 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
303};
304pub use session_schedule::{
305 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
306 SessionScheduleCapability,
307};
308pub use session_sql_database::{
309 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
310 SqlSchemaTool,
311};
312pub use session_storage::{
313 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
314 is_internal_session_kv_key,
315};
316pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
317pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
318pub use skills_scoped::{
319 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
320};
321pub use stateless_todo_list::{
322 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
323};
324pub use subagents::{SUBAGENTS_CAPABILITY_ID, SubagentCapability};
325pub use bashkit_shell::{
327 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
328};
329pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
330pub use test_math::{
331 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
332};
333pub use test_weather::{
334 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
335};
336pub use tool_call_repair::{
337 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
338 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
339 salvage_tool_arguments, tool_call_repair_capability,
340};
341pub use tool_output_distillation::{
342 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
343};
344pub use tool_output_persistence::{
345 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
346};
347pub use tool_search::{
348 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
349};
350pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
351#[cfg(feature = "web-fetch")]
352pub use web_fetch::{
353 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
354 derive_bot_auth_public_key,
355};
356
357pub struct SystemPromptContext {
367 pub session_id: SessionId,
369 pub locale: Option<String>,
371 pub file_store: Option<Arc<dyn SessionFileSystem>>,
373 pub model: Option<String>,
379}
380
381impl SystemPromptContext {
382 pub fn without_file_store(session_id: SessionId) -> Self {
384 Self {
385 session_id,
386 locale: None,
387 file_store: None,
388 model: None,
389 }
390 }
391
392 pub fn with_model(mut self, model: impl Into<String>) -> Self {
394 self.model = Some(model.into());
395 self
396 }
397}
398
399#[derive(Debug, Clone)]
451pub struct CapabilityLocalization {
452 pub locale: &'static str,
454 pub name: Option<&'static str>,
456 pub description: Option<&'static str>,
458 pub config_description: Option<&'static str>,
463 pub config_overlay: Option<serde_json::Value>,
469}
470
471impl CapabilityLocalization {
472 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
474 Self {
475 locale,
476 name: Some(name),
477 description: Some(description),
478 config_description: None,
479 config_overlay: None,
480 }
481 }
482}
483
484pub fn resolve_localized_field<T>(
488 localizations: &[CapabilityLocalization],
489 locale: Option<&str>,
490 field: impl Fn(&CapabilityLocalization) -> Option<T>,
491) -> Option<T> {
492 let mut candidates: Vec<String> = Vec::new();
493 if let Some(raw) = locale {
494 let normalized = raw.trim().replace('_', "-").to_lowercase();
495 if !normalized.is_empty() {
496 if let Some((language, _)) = normalized.split_once('-') {
497 let language = language.to_string();
498 candidates.push(normalized);
499 candidates.push(language);
500 } else {
501 candidates.push(normalized);
502 }
503 }
504 }
505 candidates.push("en".to_string());
506
507 for candidate in candidates {
508 let hit = localizations
509 .iter()
510 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
511 .and_then(&field);
512 if hit.is_some() {
513 return hit;
514 }
515 }
516 None
517}
518
519#[async_trait]
520pub trait Capability: Send + Sync {
521 fn id(&self) -> &str;
523
524 fn aliases(&self) -> Vec<&'static str> {
533 vec![]
534 }
535
536 fn name(&self) -> &str;
538
539 fn description(&self) -> &str;
541
542 fn localizations(&self) -> Vec<CapabilityLocalization> {
547 vec![]
548 }
549
550 fn localized_name(&self, locale: Option<&str>) -> String {
553 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
554 .unwrap_or_else(|| self.name())
555 .to_string()
556 }
557
558 fn localized_description(&self, locale: Option<&str>) -> String {
560 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
561 .unwrap_or_else(|| self.description())
562 .to_string()
563 }
564
565 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
569 resolve_localized_field(&self.localizations(), locale, |entry| {
570 entry.config_description
571 })
572 .map(str::to_string)
573 }
574
575 fn status(&self) -> CapabilityStatus {
577 CapabilityStatus::Available
578 }
579
580 fn icon(&self) -> Option<&str> {
582 None
583 }
584
585 fn category(&self) -> Option<&str> {
587 None
588 }
589
590 fn is_guardrail(&self) -> bool {
595 false
596 }
597
598 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
609 None
610 }
611
612 fn system_prompt_addition(&self) -> Option<&str> {
632 None
633 }
634
635 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
647 self.system_prompt_addition().map(|addition| {
648 format!(
649 "<capability id=\"{}\">\n{}\n</capability>",
650 self.id(),
651 addition
652 )
653 })
654 }
655
656 fn system_prompt_preview(&self) -> Option<String> {
662 self.system_prompt_addition().map(|s| s.to_string())
663 }
664
665 fn tools(&self) -> Vec<Box<dyn Tool>> {
667 vec![]
668 }
669
670 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
678 self.tools()
679 }
680
681 async fn system_prompt_contribution_with_config(
688 &self,
689 ctx: &SystemPromptContext,
690 _config: &serde_json::Value,
691 ) -> Option<String> {
692 self.system_prompt_contribution(ctx).await
693 }
694
695 fn tool_definitions(&self) -> Vec<ToolDefinition> {
698 self.tools().iter().map(|t| t.to_definition()).collect()
699 }
700
701 fn mounts(&self) -> Vec<MountPoint> {
709 vec![]
710 }
711
712 fn dependencies(&self) -> Vec<&'static str> {
721 vec![]
722 }
723
724 fn features(&self) -> Vec<&'static str> {
739 vec![]
740 }
741
742 fn config_schema(&self) -> Option<serde_json::Value> {
748 None
749 }
750
751 fn config_ui_schema(&self) -> Option<serde_json::Value> {
756 None
757 }
758
759 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
765 Ok(())
766 }
767
768 fn mcp_servers(&self) -> ScopedMcpServers {
774 ScopedMcpServers::default()
775 }
776
777 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
779 self.mcp_servers()
780 }
781
782 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
795 None
796 }
797
798 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
806 None
807 }
808
809 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
820 vec![]
821 }
822
823 fn pre_tool_use_hooks_with_config(
828 &self,
829 _config: &serde_json::Value,
830 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
831 self.pre_tool_use_hooks()
832 }
833
834 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
842 vec![]
843 }
844
845 fn post_tool_exec_hooks_with_config(
850 &self,
851 _config: &serde_json::Value,
852 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
853 self.post_tool_exec_hooks()
854 }
855
856 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
865 vec![]
866 }
867
868 fn tool_definition_hooks_with_config(
873 &self,
874 _config: &serde_json::Value,
875 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
876 self.tool_definition_hooks()
877 }
878
879 fn tool_definition_hooks_with_context(
889 &self,
890 _ctx: &SystemPromptContext,
891 config: &serde_json::Value,
892 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
893 self.tool_definition_hooks_with_config(config)
894 }
895
896 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
904 vec![]
905 }
906
907 fn narrate(
921 &self,
922 _tool_def: Option<&ToolDefinition>,
923 tool_call: &ToolCall,
924 phase: crate::tool_narration::ToolNarrationPhase,
925 locale: Option<&str>,
926 ) -> Option<String> {
927 self.tools()
928 .iter()
929 .find(|tool| tool.name() == tool_call.name)
930 .and_then(|tool| tool.narrate(tool_call, phase, locale))
931 }
932
933 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
949 vec![]
950 }
951
952 fn user_hooks_with_config(
958 &self,
959 _config: &serde_json::Value,
960 ) -> Vec<crate::user_hook_types::UserHookSpec> {
961 self.user_hooks()
962 }
963
964 fn risk_level(&self) -> RiskLevel {
972 RiskLevel::Low
973 }
974
975 fn commands(&self) -> Vec<CommandDescriptor> {
983 vec![]
984 }
985
986 async fn execute_command(
1000 &self,
1001 request: &ExecuteCommandRequest,
1002 _ctx: &CommandExecutionContext,
1003 ) -> crate::error::Result<CommandResult> {
1004 Err(crate::error::AgentLoopError::config(format!(
1005 "capability {} declared command /{} but does not implement execute_command",
1006 self.id(),
1007 request.name,
1008 )))
1009 }
1010
1011 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1019 vec![]
1020 }
1021
1022 fn contribute_skills(&self) -> Vec<SkillContribution> {
1032 vec![]
1033 }
1034
1035 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1046 vec![]
1047 }
1048}
1049
1050pub trait ToolDefinitionHook: Send + Sync {
1051 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1052
1053 fn applies_with_native_tool_search(&self) -> bool {
1058 true
1059 }
1060}
1061
1062pub trait ToolCallHook: Send + Sync {
1063 fn narration(
1064 &self,
1065 _tool_def: Option<&ToolDefinition>,
1066 _tool_call: &ToolCall,
1067 _phase: crate::tool_narration::ToolNarrationPhase,
1068 _locale: Option<&str>,
1069 ) -> Option<String> {
1070 None
1071 }
1072
1073 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1074 tool_call
1075 }
1076}
1077
1078pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1084
1085impl ToolCallHook for CapabilityNarrationHook {
1086 fn narration(
1087 &self,
1088 tool_def: Option<&ToolDefinition>,
1089 tool_call: &ToolCall,
1090 phase: crate::tool_narration::ToolNarrationPhase,
1091 locale: Option<&str>,
1092 ) -> Option<String> {
1093 self.0.narrate(tool_def, tool_call, phase, locale)
1094 }
1095}
1096
1097#[derive(
1101 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1102)]
1103#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1104#[cfg_attr(feature = "openapi", schema(example = "low"))]
1105#[serde(rename_all = "lowercase")]
1106pub enum RiskLevel {
1107 Low,
1109 Medium,
1111 High,
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1121#[serde(rename_all = "snake_case")]
1122pub enum BlueprintModel {
1123 Fixed(String),
1125 Default(String),
1127 Inherit,
1129}
1130
1131pub struct AgentBlueprint {
1137 pub id: &'static str,
1139 pub name: &'static str,
1141 pub description: &'static str,
1143 pub model: BlueprintModel,
1145 pub system_prompt: &'static str,
1147 pub tools: Vec<Box<dyn Tool>>,
1149 pub max_turns: Option<usize>,
1151 pub config_schema: Option<serde_json::Value>,
1153}
1154
1155impl AgentBlueprint {
1156 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1158 self.tools.iter().map(|t| t.to_definition()).collect()
1159 }
1160}
1161
1162impl std::fmt::Debug for AgentBlueprint {
1163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1164 f.debug_struct("AgentBlueprint")
1165 .field("id", &self.id)
1166 .field("name", &self.name)
1167 .field("model", &self.model)
1168 .field("tool_count", &self.tools.len())
1169 .field("max_turns", &self.max_turns)
1170 .finish()
1171 }
1172}
1173
1174#[derive(Clone)]
1201pub struct CapabilityRegistry {
1202 capabilities: HashMap<String, Arc<dyn Capability>>,
1203 aliases: HashMap<String, String>,
1205}
1206
1207impl CapabilityRegistry {
1208 pub fn new() -> Self {
1210 Self {
1211 capabilities: HashMap::new(),
1212 aliases: HashMap::new(),
1213 }
1214 }
1215
1216 pub fn with_builtins() -> Self {
1221 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1222 }
1223
1224 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1229 let mut registry = Self::new();
1230
1231 registry.register(AgentInstructionsCapability);
1233 registry.register(HumanIntentCapability);
1234 registry.register(NoopCapability);
1235 registry.register(CurrentTimeCapability);
1236 registry.register(MessageMetadataCapability);
1237 registry.register(ResearchCapability);
1238 registry.register(ModelScoutCapability);
1239 registry.register(OpenRouterWorkspaceCapability);
1240 registry.register(OpenRouterServerToolsCapability);
1241 registry.register(PlatformManagementCapability);
1242 registry.register(FileSystemCapability);
1243 registry.register(MemoryCapability);
1244 registry.register(SessionStorageCapability);
1245 registry.register(SessionCapability);
1246 registry.register(SessionSqlDatabaseCapability);
1247 registry.register(TestMathCapability);
1248 registry.register(TestWeatherCapability);
1249 registry.register(StatelessTodoListCapability);
1250 #[cfg(feature = "web-fetch")]
1251 registry.register(WebFetchCapability::from_env());
1252 registry.register(BashkitShellCapability);
1253 registry.register(BackgroundExecutionCapability);
1254 registry.register(SessionScheduleCapability);
1255 registry.register(BtwCapability);
1256 registry.register(InfinityContextCapability);
1257 registry.register(budgeting::BudgetingCapability);
1258 registry.register(SelfBudgetCapability);
1259 registry.register(CompactionCapability);
1260 registry.register(ErrorDisclosureCapability);
1261
1262 registry.register(OpenAiToolSearchCapability::new());
1264 registry.register(ClaudeToolSearchCapability::new());
1266 registry.register(ToolSearchCapability::new());
1268 registry.register(AutoToolSearchCapability::new());
1270 registry.register(PromptCachingCapability::new());
1271
1272 registry.register(ParallelToolCallsCapability);
1274
1275 registry.register(SkillsCapability);
1277
1278 registry.register(SubagentCapability);
1280
1281 registry.register(SessionTasksCapability);
1283
1284 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1288 registry.register(AgentHandoffCapability);
1289 #[cfg(feature = "a2a")]
1293 registry.register(A2aAgentDelegationCapability);
1294 }
1295
1296 registry.register(SystemCommandsCapability);
1298
1299 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1301 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1302
1303 registry.register(user_hooks::UserHooksCapability);
1306
1307 registry.register(LoopDetectionCapability);
1309
1310 registry.register(ToolCallRepairCapability);
1314
1315 registry.register(PromptCanaryGuardrailCapability);
1318
1319 registry.register(GuardrailsCapability);
1322
1323 #[cfg(feature = "ui-capabilities")]
1325 {
1326 registry.register(OpenUiCapability);
1327 registry.register(A2UiCapability);
1328 }
1329
1330 registry.register(SampleDataCapability);
1332
1333 registry.register(DataKnowledgeCapability);
1335
1336 registry.register(KnowledgeBaseCapability);
1338
1339 registry.register(KnowledgeIndexCapability);
1341
1342 registry.register(FakeWarehouseCapability);
1344 registry.register(FakeAwsCapability);
1345 registry.register(FakeCrmCapability);
1346 registry.register(FakeFinancialCapability);
1347
1348 let internal_flags = crate::InternalFeatureFlags::from_env();
1350 if internal_flags.session_sandbox {
1351 registry.register(SessionSandboxCapability);
1352 }
1353
1354 if internal_flags.lua {
1358 registry.register(LuaCapability);
1359 registry.register(LuaCodeModeCapability);
1362 }
1363 for plugin in inventory::iter::<IntegrationPlugin>() {
1364 if (!plugin.experimental_only || grade.experimental_features_enabled())
1365 && plugin
1366 .feature_flag
1367 .is_none_or(|f| internal_flags.is_enabled(f))
1368 {
1369 registry.register_boxed((plugin.factory)());
1370 }
1371 }
1372
1373 registry
1374 }
1375
1376 pub fn register(&mut self, capability: impl Capability + 'static) {
1378 self.register_arc(Arc::new(capability));
1379 }
1380
1381 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1383 self.register_arc(Arc::from(capability));
1384 }
1385
1386 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1388 let canonical = capability.id().to_string();
1389 for alias in capability.aliases() {
1390 self.aliases.insert(alias.to_string(), canonical.clone());
1391 }
1392 self.capabilities.insert(canonical, capability);
1393 }
1394
1395 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1397 self.capabilities
1398 .get(id)
1399 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1400 }
1401
1402 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1407 if self.capabilities.contains_key(id) {
1408 Some(id)
1409 } else {
1410 self.aliases
1411 .get(id)
1412 .filter(|c| self.capabilities.contains_key(*c))
1413 .map(String::as_str)
1414 }
1415 }
1416
1417 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1419 let canonical = self.canonical_id(id)?.to_string();
1420 let removed = self.capabilities.remove(&canonical);
1421 self.aliases.retain(|_, target| *target != canonical);
1422 removed
1423 }
1424
1425 pub fn has(&self, id: &str) -> bool {
1427 self.get(id).is_some()
1428 }
1429
1430 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1432 self.capabilities.values().collect()
1433 }
1434
1435 pub fn len(&self) -> usize {
1437 self.capabilities.len()
1438 }
1439
1440 pub fn is_empty(&self) -> bool {
1442 self.capabilities.is_empty()
1443 }
1444
1445 pub fn builder() -> CapabilityRegistryBuilder {
1447 CapabilityRegistryBuilder::new()
1448 }
1449
1450 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1454 for cap in self.capabilities.values() {
1455 for bp in cap.agent_blueprints() {
1456 if bp.id == id {
1457 return Some(bp);
1458 }
1459 }
1460 }
1461 None
1462 }
1463
1464 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1468 for (capability_id, cap) in &self.capabilities {
1469 for bp in cap.agent_blueprints() {
1470 if bp.id == id {
1471 return Some((capability_id.clone(), bp));
1472 }
1473 }
1474 }
1475 None
1476 }
1477
1478 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1480 self.capabilities
1481 .values()
1482 .flat_map(|cap| cap.agent_blueprints())
1483 .collect()
1484 }
1485}
1486
1487impl Default for CapabilityRegistry {
1488 fn default() -> Self {
1489 Self::with_builtins()
1490 }
1491}
1492
1493impl std::fmt::Debug for CapabilityRegistry {
1494 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1495 let ids: Vec<_> = self.capabilities.keys().collect();
1496 f.debug_struct("CapabilityRegistry")
1497 .field("capabilities", &ids)
1498 .finish()
1499 }
1500}
1501
1502pub struct CapabilityRegistryBuilder {
1504 registry: CapabilityRegistry,
1505}
1506
1507impl CapabilityRegistryBuilder {
1508 pub fn new() -> Self {
1510 Self {
1511 registry: CapabilityRegistry::new(),
1512 }
1513 }
1514
1515 pub fn with_builtins() -> Self {
1517 Self {
1518 registry: CapabilityRegistry::with_builtins(),
1519 }
1520 }
1521
1522 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1524 self.registry.register(capability);
1525 self
1526 }
1527
1528 pub fn build(self) -> CapabilityRegistry {
1530 self.registry
1531 }
1532}
1533
1534impl Default for CapabilityRegistryBuilder {
1535 fn default() -> Self {
1536 Self::new()
1537 }
1538}
1539
1540pub struct ModelViewContext<'a> {
1546 pub session_id: SessionId,
1547 pub prior_usage: Option<&'a TokenUsage>,
1548}
1549
1550pub trait ModelViewProvider: Send + Sync {
1556 fn apply_model_view(
1557 &self,
1558 messages: Vec<Message>,
1559 config: &serde_json::Value,
1560 context: &ModelViewContext<'_>,
1561 ) -> Vec<Message>;
1562
1563 fn priority(&self) -> i32 {
1564 0
1565 }
1566}
1567
1568pub struct CollectedCapabilities {
1573 pub system_prompt_parts: Vec<String>,
1575 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1577 pub tools: Vec<Box<dyn Tool>>,
1579 pub tool_definitions: Vec<ToolDefinition>,
1581 pub mounts: Vec<MountPoint>,
1583 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1585 pub applied_ids: Vec<String>,
1587 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1589 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1591 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1594 pub parallel_tool_calls: Option<bool>,
1598 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1600 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1602 pub mcp_servers: ScopedMcpServers,
1604 }
1610
1611#[derive(Debug, Clone, PartialEq, Eq)]
1612pub struct SystemPromptAttribution {
1613 pub capability_id: String,
1614 pub content: String,
1615}
1616
1617impl CollectedCapabilities {
1618 pub fn system_prompt_prefix(&self) -> Option<String> {
1621 if self.system_prompt_parts.is_empty() {
1622 None
1623 } else {
1624 Some(self.system_prompt_parts.join("\n\n"))
1625 }
1626 }
1627
1628 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1632 for (provider, config) in &self.message_filter_providers {
1634 provider.apply_filters(query, config);
1635 }
1636 }
1637
1638 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1641 for (provider, config) in &self.message_filter_providers {
1642 provider.post_load(messages, config);
1643 }
1644 }
1645
1646 pub fn has_message_filters(&self) -> bool {
1648 !self.message_filter_providers.is_empty()
1649 }
1650}
1651
1652pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1657 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1658 return base_system_prompt.to_string();
1659 };
1660
1661 if base_system_prompt.is_empty() {
1662 return additions.to_string();
1663 }
1664
1665 if base_system_prompt.contains("<system-prompt>") {
1666 format!("{base_system_prompt}\n\n{additions}")
1667 } else {
1668 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1669 }
1670}
1671
1672pub struct CollectedMessageFilters {
1679 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1681}
1682
1683pub struct CollectedModelViewProviders {
1685 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1687}
1688
1689impl CollectedMessageFilters {
1695 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1697 for (provider, config) in &self.message_filter_providers {
1698 provider.apply_filters(query, config);
1699 }
1700 }
1701
1702 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1704 for (provider, config) in &self.message_filter_providers {
1705 provider.post_load(messages, config);
1706 }
1707 }
1708}
1709
1710impl CollectedModelViewProviders {
1711 pub fn apply_model_view(
1713 &self,
1714 mut messages: Vec<Message>,
1715 context: &ModelViewContext<'_>,
1716 ) -> Vec<Message> {
1717 for (provider, config) in &self.model_view_providers {
1718 messages = provider.apply_model_view(messages, config, context);
1719 }
1720 messages
1721 }
1722}
1723
1724fn compaction_is_enabled(
1730 capability_configs: &[AgentCapabilityConfig],
1731 registry: &CapabilityRegistry,
1732) -> bool {
1733 capability_configs.iter().any(|cap_config| {
1734 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
1735 && registry
1736 .get(cap_config.capability_ref.as_str())
1737 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
1738 })
1739}
1740
1741fn message_filter_config_for(
1750 cap_id: &str,
1751 base: &serde_json::Value,
1752 compaction_on: bool,
1753) -> serde_json::Value {
1754 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
1755 return base.clone();
1756 }
1757 let mut config = base.clone();
1758 match config.as_object_mut() {
1759 Some(map) => {
1760 map.insert(
1761 "compaction_active".to_string(),
1762 serde_json::Value::Bool(true),
1763 );
1764 }
1765 None => {
1766 config = serde_json::json!({ "compaction_active": true });
1767 }
1768 }
1769 config
1770}
1771
1772pub fn collect_message_filters_only(
1778 capability_configs: &[AgentCapabilityConfig],
1779 registry: &CapabilityRegistry,
1780) -> CollectedMessageFilters {
1781 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1782 Vec::new();
1783 let compaction_on = compaction_is_enabled(capability_configs, registry);
1784
1785 for cap_config in capability_configs {
1786 let cap_id = cap_config.capability_ref.as_str();
1787 if let Some(capability) = registry.get(cap_id) {
1788 if capability.status() != CapabilityStatus::Available {
1789 continue;
1790 }
1791 let effective: &dyn Capability = capability
1794 .resolve_for_model(None)
1795 .unwrap_or_else(|| capability.as_ref());
1796 if let Some(provider) = effective.message_filter_provider() {
1797 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
1798 message_filter_providers.push((provider, config));
1799 }
1800 }
1801 }
1802
1803 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1804
1805 CollectedMessageFilters {
1806 message_filter_providers,
1807 }
1808}
1809
1810pub fn collect_model_view_providers(
1817 capability_configs: &[AgentCapabilityConfig],
1818 registry: &CapabilityRegistry,
1819 model: Option<&str>,
1820) -> CollectedModelViewProviders {
1821 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1822
1823 for cap_config in capability_configs {
1824 let cap_id = cap_config.capability_ref.as_str();
1825 if let Some(capability) = registry.get(cap_id) {
1826 if capability.status() != CapabilityStatus::Available {
1827 continue;
1828 }
1829 let effective: &dyn Capability = capability
1830 .resolve_for_model(model)
1831 .unwrap_or_else(|| capability.as_ref());
1832 if let Some(provider) = effective.model_view_provider() {
1833 model_view_providers.push((provider, cap_config.config.clone()));
1834 }
1835 }
1836 }
1837
1838 model_view_providers.sort_by_key(|(p, _)| p.priority());
1839
1840 CollectedModelViewProviders {
1841 model_view_providers,
1842 }
1843}
1844
1845pub fn collect_capability_mcp_servers(
1846 capability_configs: &[AgentCapabilityConfig],
1847 registry: &CapabilityRegistry,
1848) -> ScopedMcpServers {
1849 let mut servers = ScopedMcpServers::default();
1850
1851 for cap_config in capability_configs {
1852 let cap_id = cap_config.capability_ref.as_str();
1853 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
1856 if let Ok(definition) =
1857 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1858 {
1859 if definition.status != CapabilityStatus::Available {
1860 continue;
1861 }
1862 if let Some(contributed) = definition.mcp_servers {
1863 servers = merge_scoped_mcp_servers(&servers, &contributed);
1864 }
1865 }
1866 continue;
1867 }
1868 if let Some(capability) = registry.get(cap_id) {
1869 if capability.status() != CapabilityStatus::Available {
1870 continue;
1871 }
1872 servers = merge_scoped_mcp_servers(
1873 &servers,
1874 &capability.mcp_servers_with_config(&cap_config.config),
1875 );
1876 }
1877 }
1878
1879 servers
1880}
1881
1882pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1889
1890#[derive(Debug, Clone, PartialEq, Eq)]
1892pub enum DependencyError {
1893 CircularDependency {
1895 capability_id: String,
1897 chain: Vec<String>,
1899 },
1900 TooManyCapabilities {
1902 count: usize,
1904 max: usize,
1906 },
1907}
1908
1909impl std::fmt::Display for DependencyError {
1910 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1911 match self {
1912 DependencyError::CircularDependency {
1913 capability_id,
1914 chain,
1915 } => {
1916 write!(
1917 f,
1918 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1919 capability_id,
1920 chain.join(" -> "),
1921 capability_id
1922 )
1923 }
1924 DependencyError::TooManyCapabilities { count, max } => {
1925 write!(
1926 f,
1927 "Too many capabilities after resolution: {} (max: {})",
1928 count, max
1929 )
1930 }
1931 }
1932 }
1933}
1934
1935impl std::error::Error for DependencyError {}
1936
1937#[derive(Debug, Clone)]
1939pub struct ResolvedCapabilities {
1940 pub resolved_ids: Vec<String>,
1943 pub added_as_dependencies: Vec<String>,
1945 pub user_selected: Vec<String>,
1947}
1948
1949pub fn resolve_dependencies(
1969 selected_ids: &[String],
1970 registry: &CapabilityRegistry,
1971) -> Result<ResolvedCapabilities, DependencyError> {
1972 use std::collections::HashSet;
1973
1974 let user_selected: HashSet<String> = selected_ids
1976 .iter()
1977 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
1978 .collect();
1979 let mut resolved: Vec<String> = Vec::new();
1980 let mut resolved_set: HashSet<String> = HashSet::new();
1981 let mut added_as_dependencies: Vec<String> = Vec::new();
1982
1983 for cap_id in selected_ids {
1985 resolve_single_capability(
1986 cap_id,
1987 registry,
1988 &mut resolved,
1989 &mut resolved_set,
1990 &mut added_as_dependencies,
1991 &user_selected,
1992 &mut Vec::new(), )?;
1994 }
1995
1996 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
1998 return Err(DependencyError::TooManyCapabilities {
1999 count: resolved.len(),
2000 max: MAX_RESOLVED_CAPABILITIES,
2001 });
2002 }
2003
2004 Ok(ResolvedCapabilities {
2005 resolved_ids: resolved,
2006 added_as_dependencies,
2007 user_selected: selected_ids.to_vec(),
2008 })
2009}
2010
2011pub fn resolve_capability_configs(
2016 selected_configs: &[AgentCapabilityConfig],
2017 registry: &CapabilityRegistry,
2018) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2019 let mut selected_ids: Vec<String> = Vec::new();
2020 for config in selected_configs {
2021 if (is_declarative_capability(config.capability_id())
2024 || is_plugin_capability(config.capability_id()))
2025 && let Ok(definition) =
2026 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2027 {
2028 selected_ids.extend(definition.dependencies);
2029 }
2030 selected_ids.push(config.capability_id().to_string());
2031 }
2032 let resolved = resolve_dependencies(&selected_ids, registry)?;
2033
2034 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2037 .iter()
2038 .map(|config| {
2039 let id = config.capability_id();
2040 let id = registry.canonical_id(id).unwrap_or(id);
2041 (id.to_string(), config.config.clone())
2042 })
2043 .collect();
2044
2045 Ok(resolved
2046 .resolved_ids
2047 .into_iter()
2048 .map(|capability_id| {
2049 explicit_configs
2050 .get(&capability_id)
2051 .cloned()
2052 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2053 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2054 })
2055 .collect())
2056}
2057
2058fn resolve_single_capability(
2060 cap_id: &str,
2061 registry: &CapabilityRegistry,
2062 resolved: &mut Vec<String>,
2063 resolved_set: &mut std::collections::HashSet<String>,
2064 added_as_dependencies: &mut Vec<String>,
2065 user_selected: &std::collections::HashSet<String>,
2066 visiting: &mut Vec<String>,
2067) -> Result<(), DependencyError> {
2068 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2072
2073 if resolved_set.contains(cap_id) {
2075 return Ok(());
2076 }
2077
2078 if visiting.contains(&cap_id.to_string()) {
2080 return Err(DependencyError::CircularDependency {
2081 capability_id: cap_id.to_string(),
2082 chain: visiting.clone(),
2083 });
2084 }
2085
2086 let capability = match registry.get(cap_id) {
2088 Some(cap) => cap,
2089 None => {
2090 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2094 && !resolved_set.contains(cap_id)
2095 {
2096 resolved.push(cap_id.to_string());
2097 resolved_set.insert(cap_id.to_string());
2098 if !user_selected.contains(cap_id) {
2099 added_as_dependencies.push(cap_id.to_string());
2100 }
2101 }
2102 return Ok(());
2103 }
2104 };
2105
2106 visiting.push(cap_id.to_string());
2108
2109 for dep_id in capability.dependencies() {
2111 resolve_single_capability(
2112 dep_id,
2113 registry,
2114 resolved,
2115 resolved_set,
2116 added_as_dependencies,
2117 user_selected,
2118 visiting,
2119 )?;
2120 }
2121
2122 visiting.pop();
2124
2125 if !resolved_set.contains(cap_id) {
2127 resolved.push(cap_id.to_string());
2128 resolved_set.insert(cap_id.to_string());
2129
2130 if !user_selected.contains(cap_id) {
2132 added_as_dependencies.push(cap_id.to_string());
2133 }
2134 }
2135
2136 Ok(())
2137}
2138
2139pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2144 use std::collections::HashSet;
2145
2146 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2147 Ok(resolved) => resolved.resolved_ids,
2148 Err(_) => capability_ids.to_vec(),
2149 };
2150
2151 let mut seen = HashSet::new();
2152 let mut features = Vec::new();
2153 for cap_id in &resolved_ids {
2154 if let Some(cap) = registry.get(cap_id) {
2155 for feature in cap.features() {
2156 if seen.insert(feature) {
2157 features.push(feature.to_string());
2158 }
2159 }
2160 }
2161 }
2162 features
2163}
2164
2165pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2168 registry
2169 .get(cap_id)
2170 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2171 .unwrap_or_default()
2172}
2173
2174pub async fn collect_capabilities(
2190 capability_ids: &[String],
2191 registry: &CapabilityRegistry,
2192 ctx: &SystemPromptContext,
2193) -> CollectedCapabilities {
2194 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2197 Ok(resolved) => resolved.resolved_ids,
2198 Err(e) => {
2199 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2200 capability_ids.to_vec()
2201 }
2202 };
2203
2204 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2206 .iter()
2207 .map(|id| AgentCapabilityConfig {
2208 capability_ref: CapabilityId::new(id),
2209 config: serde_json::Value::Object(serde_json::Map::new()),
2210 })
2211 .collect();
2212
2213 collect_capabilities_with_configs(&configs, registry, ctx).await
2214}
2215
2216pub async fn collect_capabilities_with_configs(
2227 capability_configs: &[AgentCapabilityConfig],
2228 registry: &CapabilityRegistry,
2229 ctx: &SystemPromptContext,
2230) -> CollectedCapabilities {
2231 let mut system_prompt_parts: Vec<String> = Vec::new();
2232 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2233 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2234 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2235 let mut mounts: Vec<MountPoint> = Vec::new();
2236 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2237 Vec::new();
2238 let mut applied_ids: Vec<String> = Vec::new();
2239 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2240 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2241 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2242 let mut parallel_tool_calls: Option<bool> = None;
2243 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2244 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2245 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2248 let mut mcp_servers = ScopedMcpServers::default();
2249 let compaction_on = compaction_is_enabled(capability_configs, registry);
2250
2251 for cap_config in capability_configs {
2252 let cap_id = cap_config.capability_ref.as_str();
2253 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2258 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2259 cap_config.config.clone(),
2260 ) {
2261 Ok(definition) => {
2262 if definition.status != CapabilityStatus::Available {
2263 continue;
2264 }
2265
2266 if let Some(prompt) = definition.system_prompt.as_deref() {
2267 let contribution =
2268 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2269 system_prompt_attributions.push(SystemPromptAttribution {
2270 capability_id: cap_id.to_string(),
2271 content: contribution.clone(),
2272 });
2273 system_prompt_parts.push(contribution);
2274 }
2275
2276 mounts.extend(definition.mounts(cap_id));
2277 if let Some(ref servers) = definition.mcp_servers {
2278 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2279 }
2280 for skill in definition.skill_contributions() {
2281 mounts.push(skill.to_mount(cap_id));
2282 }
2283
2284 applied_ids.push(cap_id.to_string());
2285 }
2286 Err(error) => {
2287 tracing::warn!(
2288 capability_id = %cap_id,
2289 error = %error,
2290 "Skipping invalid declarative/plugin capability config"
2291 );
2292 }
2293 }
2294 continue;
2295 }
2296 if let Some(capability) = registry.get(cap_id) {
2297 if capability.status() != CapabilityStatus::Available {
2299 continue;
2300 }
2301
2302 let effective: &dyn Capability =
2314 match capability.resolve_for_model(ctx.model.as_deref()) {
2315 Some(inner) => inner,
2316 None => capability.as_ref(),
2317 };
2318 let effective_id = effective.id();
2319
2320 if let Some(contribution) = effective
2322 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2323 .await
2324 {
2325 system_prompt_attributions.push(SystemPromptAttribution {
2326 capability_id: cap_id.to_string(),
2327 content: contribution.clone(),
2328 });
2329 system_prompt_parts.push(contribution);
2330 }
2331
2332 tools.extend(effective.tools_with_config(&cap_config.config));
2334 tool_definition_hooks
2335 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2336 tool_call_hooks.extend(effective.tool_call_hooks());
2337 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2339 let cap_category = effective.category();
2344 for def in effective.tool_definitions() {
2345 let def = match (def.category(), cap_category) {
2346 (None, Some(cat)) => def.with_category(cat),
2347 _ => def,
2348 }
2349 .with_capability_attribution(cap_id, Some(capability.name()));
2350 tool_definitions.push(def);
2351 }
2352
2353 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2361 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2362 {
2363 let threshold = cap_config
2365 .config
2366 .get("threshold")
2367 .and_then(|v| v.as_u64())
2368 .map(|v| v as usize)
2369 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2370 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2371 enabled: true,
2372 threshold,
2373 });
2374 }
2375
2376 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2377 let strategy = cap_config
2378 .config
2379 .get("strategy")
2380 .and_then(|v| v.as_str())
2381 .map(|value| match value {
2382 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2383 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2384 })
2385 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2386 let gemini_cached_content = cap_config
2387 .config
2388 .get("gemini_cached_content")
2389 .and_then(|v| v.as_str())
2390 .map(str::to_string);
2391 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2392 enabled: true,
2393 strategy,
2394 gemini_cached_content,
2395 });
2396 }
2397
2398 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2399 parallel_tool_calls =
2400 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2401 }
2402
2403 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2404 let server_tools =
2405 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2406 if !server_tools.is_empty() {
2407 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2408 server_tools,
2409 ..Default::default()
2410 });
2411 }
2412 }
2413
2414 mounts.extend(effective.mounts());
2416
2417 mcp_servers = merge_scoped_mcp_servers(
2418 &mcp_servers,
2419 &effective.mcp_servers_with_config(&cap_config.config),
2420 );
2421
2422 for skill in effective.contribute_skills() {
2426 mounts.push(skill.to_mount(cap_id));
2427 }
2428
2429 if let Some(provider) = effective.message_filter_provider() {
2431 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2432 message_filter_providers.push((provider, config));
2433 }
2434
2435 applied_ids.push(cap_id.to_string());
2436 }
2437 }
2438
2439 if !applied_ids
2451 .iter()
2452 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2453 && tool_definitions
2454 .iter()
2455 .any(|def| def.hints().supports_background == Some(true))
2456 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2457 && bg_cap.status() == CapabilityStatus::Available
2458 {
2459 tools.extend(bg_cap.tools());
2460 let cap_category = bg_cap.category();
2461 for def in bg_cap.tool_definitions() {
2462 let def = match (def.category(), cap_category) {
2463 (None, Some(cat)) => def.with_category(cat),
2464 _ => def,
2465 }
2466 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2467 tool_definitions.push(def);
2468 }
2469 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2470 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2471 }
2472
2473 tool_call_hooks.extend(narration_hooks);
2477
2478 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2480
2481 CollectedCapabilities {
2482 system_prompt_parts,
2483 system_prompt_attributions,
2484 tools,
2485 tool_definitions,
2486 mounts,
2487 message_filter_providers,
2488 applied_ids,
2489 tool_search,
2490 prompt_cache,
2491 openrouter_routing,
2492 parallel_tool_calls,
2493 tool_definition_hooks,
2494 tool_call_hooks,
2495 mcp_servers,
2496 }
2497}
2498
2499pub struct AppliedCapabilities {
2505 pub runtime_agent: RuntimeAgent,
2507 pub tool_registry: ToolRegistry,
2509 pub applied_ids: Vec<String>,
2511}
2512
2513pub async fn apply_capabilities(
2550 base_runtime_agent: RuntimeAgent,
2551 capability_ids: &[String],
2552 registry: &CapabilityRegistry,
2553 ctx: &SystemPromptContext,
2554) -> AppliedCapabilities {
2555 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2556
2557 let final_system_prompt = compose_system_prompt(
2559 &base_runtime_agent.system_prompt,
2560 collected.system_prompt_prefix().as_deref(),
2561 );
2562
2563 let mut tool_registry = ToolRegistry::new();
2565 for tool in collected.tools {
2566 tool_registry.register_boxed(tool);
2567 }
2568
2569 let mut tools = collected.tool_definitions;
2571 for hook in &collected.tool_definition_hooks {
2572 tools = hook.transform(tools);
2573 }
2574
2575 let runtime_agent = RuntimeAgent {
2576 system_prompt: final_system_prompt,
2577 model: base_runtime_agent.model,
2578 tools,
2579 max_iterations: base_runtime_agent.max_iterations,
2580 temperature: base_runtime_agent.temperature,
2581 max_tokens: base_runtime_agent.max_tokens,
2582 tool_search: collected.tool_search,
2583 prompt_cache: collected.prompt_cache,
2584 openrouter_routing: collected.openrouter_routing,
2585 network_access: base_runtime_agent.network_access,
2586 parallel_tool_calls: base_runtime_agent
2589 .parallel_tool_calls
2590 .or(collected.parallel_tool_calls),
2591 };
2592
2593 AppliedCapabilities {
2594 runtime_agent,
2595 tool_registry,
2596 applied_ids: collected.applied_ids,
2597 }
2598}
2599
2600#[cfg(test)]
2605mod tests {
2606 use super::*;
2607 use crate::typed_id::SessionId;
2608 use std::collections::BTreeSet;
2609 use uuid::Uuid;
2610
2611 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2613
2614 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2615 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2616 }
2617
2618 fn test_ctx() -> SystemPromptContext {
2620 SystemPromptContext::without_file_store(SessionId::new())
2621 }
2622
2623 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2625 let mut ids = [
2626 "agent_instructions",
2627 "human_intent",
2628 "budgeting",
2629 "self_budget",
2630 "noop",
2631 "current_time",
2632 "research",
2633 "platform_management",
2634 "session_file_system",
2635 "session_storage",
2636 "session",
2637 "session_sql_database",
2638 "test_math",
2639 "test_weather",
2640 "stateless_todo_list",
2641 "web_fetch",
2642 "bashkit_shell",
2643 "background_execution",
2644 "session_schedule",
2645 "btw",
2646 "infinity_context",
2647 "compaction",
2648 "memory",
2649 "message_metadata",
2650 "openai_tool_search",
2651 "claude_tool_search",
2652 "tool_search",
2653 "auto_tool_search",
2654 "prompt_caching",
2655 "parallel_tool_calls",
2656 "session_tasks",
2657 "skills",
2658 "subagents",
2659 "system_commands",
2660 "sample_data",
2661 "data_knowledge",
2662 "knowledge_base",
2663 "knowledge_index",
2664 "tool_output_persistence",
2665 "tool_output_distillation",
2666 "fake_warehouse",
2667 "fake_aws",
2668 "fake_crm",
2669 "fake_financial",
2670 "loop_detection",
2671 "tool_call_repair",
2672 "error_disclosure",
2673 "prompt_canary_guardrail",
2674 "guardrails",
2675 "user_hooks",
2676 "model_scout",
2677 "openrouter_workspace",
2678 "openrouter_server_tools",
2679 ]
2680 .into_iter()
2681 .collect::<BTreeSet<_>>();
2682 if cfg!(feature = "ui-capabilities") {
2683 ids.insert("openui");
2684 ids.insert("a2ui");
2685 }
2686 ids
2687 }
2688
2689 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2691 let mut ids = expected_core_builtin_ids();
2692 ids.insert("agent_handoff");
2693 ids.insert("a2a_agent_delegation");
2694 ids
2695 }
2696
2697 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2698 registry.capabilities.keys().map(String::as_str).collect()
2699 }
2700
2701 #[test]
2711 fn test_capability_registry_with_builtins_dev() {
2712 let _lock = lock_env();
2714 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2715 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2716 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2717 assert!(registry.has("agent_handoff"));
2718 assert!(registry.has("a2a_agent_delegation"));
2719 }
2720
2721 #[test]
2722 fn test_capability_registry_with_builtins_prod() {
2723 let _lock = lock_env();
2725 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2726 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2727 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2728 assert!(!registry.has("docker_container"));
2730 assert!(!registry.has("agent_handoff"));
2731 assert!(!registry.has("a2a_agent_delegation"));
2732 }
2733
2734 #[test]
2735 fn test_agent_delegation_enabled_by_env_in_prod() {
2736 let _lock = lock_env();
2738 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2739 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2740 assert!(registry.has("agent_handoff"));
2741 assert!(registry.has("a2a_agent_delegation"));
2742 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2743 }
2744
2745 #[test]
2746 fn test_agent_delegation_disabled_by_env_in_dev() {
2747 let _lock = lock_env();
2749 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2750 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2751 assert!(!registry.has("agent_handoff"));
2752 assert!(!registry.has("a2a_agent_delegation"));
2753 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2754 }
2755
2756 #[test]
2757 fn test_capability_registry_get() {
2758 let registry = CapabilityRegistry::with_builtins();
2759
2760 let noop = registry.get("noop").unwrap();
2761 assert_eq!(noop.id(), "noop");
2762 assert_eq!(noop.name(), "No-Op");
2763 assert_eq!(noop.status(), CapabilityStatus::Available);
2764 }
2765
2766 #[test]
2767 fn test_capability_registry_blueprint_with_capability() {
2768 struct BlueprintProviderCapability;
2769
2770 impl Capability for BlueprintProviderCapability {
2771 fn id(&self) -> &str {
2772 "blueprint_provider"
2773 }
2774 fn name(&self) -> &str {
2775 "Blueprint Provider"
2776 }
2777 fn description(&self) -> &str {
2778 "Capability that provides a blueprint for tests"
2779 }
2780 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2781 vec![AgentBlueprint {
2782 id: "test_blueprint",
2783 name: "Test Blueprint",
2784 description: "Blueprint for capability registry tests",
2785 model: BlueprintModel::Inherit,
2786 system_prompt: "Test prompt",
2787 tools: vec![],
2788 max_turns: None,
2789 config_schema: None,
2790 }]
2791 }
2792 }
2793
2794 let mut registry = CapabilityRegistry::new();
2795 registry.register(BlueprintProviderCapability);
2796
2797 let (capability_id, blueprint) = registry
2798 .blueprint_with_capability("test_blueprint")
2799 .expect("blueprint should resolve with capability id");
2800 assert_eq!(capability_id, "blueprint_provider");
2801 assert_eq!(blueprint.id, "test_blueprint");
2802 }
2803
2804 #[test]
2805 fn test_capability_registry_builder() {
2806 let registry = CapabilityRegistry::builder()
2807 .capability(NoopCapability)
2808 .capability(CurrentTimeCapability)
2809 .build();
2810
2811 assert!(registry.has("noop"));
2812 assert!(registry.has("current_time"));
2813 assert_eq!(registry.len(), 2);
2814 }
2815
2816 #[test]
2817 fn test_capability_status() {
2818 let registry = CapabilityRegistry::with_builtins();
2819
2820 let current_time = registry.get("current_time").unwrap();
2821 assert_eq!(current_time.status(), CapabilityStatus::Available);
2822
2823 let research = registry.get("research").unwrap();
2824 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2825 }
2826
2827 #[test]
2828 fn test_capability_icons_and_categories() {
2829 let registry = CapabilityRegistry::with_builtins();
2830
2831 let noop = registry.get("noop").unwrap();
2832 assert_eq!(noop.icon(), Some("circle-off"));
2833 assert_eq!(noop.category(), Some("Testing"));
2834
2835 let current_time = registry.get("current_time").unwrap();
2836 assert_eq!(current_time.icon(), Some("clock"));
2837 assert_eq!(current_time.category(), Some("Core"));
2838 }
2839
2840 #[test]
2841 fn test_system_prompt_preview_default_delegates_to_addition() {
2842 let registry = CapabilityRegistry::with_builtins();
2843
2844 let test_math = registry.get("test_math").unwrap();
2846 assert_eq!(
2847 test_math.system_prompt_preview().as_deref(),
2848 test_math.system_prompt_addition()
2849 );
2850
2851 let current_time = registry.get("current_time").unwrap();
2853 assert!(current_time.system_prompt_preview().is_none());
2854 assert!(current_time.system_prompt_addition().is_none());
2855 }
2856
2857 #[test]
2858 fn test_system_prompt_preview_dynamic_capability() {
2859 let registry = CapabilityRegistry::with_builtins();
2860 let cap = registry.get("agent_instructions").unwrap();
2861
2862 assert!(cap.system_prompt_addition().is_none());
2864 assert!(cap.system_prompt_preview().is_some());
2865 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2866 }
2867
2868 #[tokio::test]
2873 async fn test_apply_capabilities_empty() {
2874 let registry = CapabilityRegistry::with_builtins();
2875 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2876
2877 let applied =
2878 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
2879
2880 assert_eq!(
2881 applied.runtime_agent.system_prompt,
2882 base_runtime_agent.system_prompt
2883 );
2884 assert!(applied.tool_registry.is_empty());
2885 assert!(applied.applied_ids.is_empty());
2886 }
2887
2888 #[tokio::test]
2889 async fn test_apply_capabilities_noop() {
2890 let registry = CapabilityRegistry::with_builtins();
2891 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2892
2893 let applied = apply_capabilities(
2894 base_runtime_agent.clone(),
2895 &["noop".to_string()],
2896 ®istry,
2897 &test_ctx(),
2898 )
2899 .await;
2900
2901 assert_eq!(
2903 applied.runtime_agent.system_prompt,
2904 base_runtime_agent.system_prompt
2905 );
2906 assert!(applied.tool_registry.is_empty());
2907 assert_eq!(applied.applied_ids, vec!["noop"]);
2908 }
2909
2910 #[tokio::test]
2911 async fn test_apply_capabilities_current_time() {
2912 let registry = CapabilityRegistry::with_builtins();
2913 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2914
2915 let applied = apply_capabilities(
2916 base_runtime_agent.clone(),
2917 &["current_time".to_string()],
2918 ®istry,
2919 &test_ctx(),
2920 )
2921 .await;
2922
2923 assert_eq!(
2925 applied.runtime_agent.system_prompt,
2926 base_runtime_agent.system_prompt
2927 );
2928 assert!(applied.tool_registry.has("get_current_time"));
2929 assert_eq!(applied.tool_registry.len(), 1);
2930 assert_eq!(applied.applied_ids, vec!["current_time"]);
2931 }
2932
2933 #[tokio::test]
2934 async fn test_apply_capabilities_skips_coming_soon() {
2935 let registry = CapabilityRegistry::with_builtins();
2936 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2937
2938 let applied = apply_capabilities(
2940 base_runtime_agent.clone(),
2941 &["research".to_string()],
2942 ®istry,
2943 &test_ctx(),
2944 )
2945 .await;
2946
2947 assert_eq!(
2949 applied.runtime_agent.system_prompt,
2950 base_runtime_agent.system_prompt
2951 );
2952 assert!(applied.applied_ids.is_empty()); }
2954
2955 #[tokio::test]
2956 async fn test_apply_capabilities_multiple() {
2957 let registry = CapabilityRegistry::with_builtins();
2958 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2959
2960 let applied = apply_capabilities(
2961 base_runtime_agent.clone(),
2962 &["noop".to_string(), "current_time".to_string()],
2963 ®istry,
2964 &test_ctx(),
2965 )
2966 .await;
2967
2968 assert!(applied.tool_registry.has("get_current_time"));
2969 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
2970 }
2971
2972 #[tokio::test]
2973 async fn test_apply_capabilities_preserves_order() {
2974 let registry = CapabilityRegistry::with_builtins();
2975 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
2976
2977 let applied = apply_capabilities(
2979 base_runtime_agent,
2980 &["current_time".to_string(), "noop".to_string()],
2981 ®istry,
2982 &test_ctx(),
2983 )
2984 .await;
2985
2986 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
2987 }
2988
2989 #[tokio::test]
2990 async fn test_apply_capabilities_test_math() {
2991 let registry = CapabilityRegistry::with_builtins();
2992 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2993
2994 let applied = apply_capabilities(
2995 base_runtime_agent.clone(),
2996 &["test_math".to_string()],
2997 ®istry,
2998 &test_ctx(),
2999 )
3000 .await;
3001
3002 assert!(
3004 !applied
3005 .runtime_agent
3006 .system_prompt
3007 .contains("<capability id=\"test_math\">")
3008 );
3009 assert!(
3011 applied
3012 .runtime_agent
3013 .system_prompt
3014 .contains("You are a helpful assistant.")
3015 );
3016 assert!(applied.tool_registry.has("add"));
3017 assert!(applied.tool_registry.has("subtract"));
3018 assert!(applied.tool_registry.has("multiply"));
3019 assert!(applied.tool_registry.has("divide"));
3020 assert_eq!(applied.tool_registry.len(), 4);
3021 }
3022
3023 #[tokio::test]
3024 async fn test_apply_capabilities_test_weather() {
3025 let registry = CapabilityRegistry::with_builtins();
3026 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3027
3028 let applied = apply_capabilities(
3029 base_runtime_agent.clone(),
3030 &["test_weather".to_string()],
3031 ®istry,
3032 &test_ctx(),
3033 )
3034 .await;
3035
3036 assert!(
3038 !applied
3039 .runtime_agent
3040 .system_prompt
3041 .contains("<capability id=\"test_weather\">")
3042 );
3043 assert!(applied.tool_registry.has("get_weather"));
3044 assert!(applied.tool_registry.has("get_forecast"));
3045 assert_eq!(applied.tool_registry.len(), 2);
3046 }
3047
3048 #[tokio::test]
3049 async fn test_apply_capabilities_test_math_and_test_weather() {
3050 let registry = CapabilityRegistry::with_builtins();
3051 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3052
3053 let applied = apply_capabilities(
3054 base_runtime_agent.clone(),
3055 &["test_math".to_string(), "test_weather".to_string()],
3056 ®istry,
3057 &test_ctx(),
3058 )
3059 .await;
3060
3061 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3064 assert!(applied.tool_registry.has("get_weather"));
3065 }
3066
3067 #[tokio::test]
3068 async fn test_apply_capabilities_stateless_todo_list() {
3069 let registry = CapabilityRegistry::with_builtins();
3070 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3071
3072 let applied = apply_capabilities(
3073 base_runtime_agent.clone(),
3074 &["stateless_todo_list".to_string()],
3075 ®istry,
3076 &test_ctx(),
3077 )
3078 .await;
3079
3080 assert!(
3082 applied
3083 .runtime_agent
3084 .system_prompt
3085 .contains("Task Management")
3086 );
3087 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3088 assert!(applied.tool_registry.has("write_todos"));
3089 assert_eq!(applied.tool_registry.len(), 1);
3090 }
3091
3092 #[tokio::test]
3093 async fn test_apply_capabilities_web_fetch() {
3094 let registry = CapabilityRegistry::with_builtins();
3095 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3096
3097 let applied = apply_capabilities(
3098 base_runtime_agent.clone(),
3099 &["web_fetch".to_string()],
3100 ®istry,
3101 &test_ctx(),
3102 )
3103 .await;
3104
3105 assert!(
3107 applied
3108 .runtime_agent
3109 .system_prompt
3110 .contains(&base_runtime_agent.system_prompt)
3111 );
3112 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3113 assert!(applied.tool_registry.has("web_fetch"));
3114 assert_eq!(applied.tool_registry.len(), 1);
3115 }
3116
3117 #[tokio::test]
3122 async fn test_xml_tags_wrap_capability_prompts() {
3123 let registry = CapabilityRegistry::with_builtins();
3124 let collected =
3125 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3126 .await;
3127
3128 assert_eq!(collected.system_prompt_parts.len(), 1);
3129 let part = &collected.system_prompt_parts[0];
3130 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3131 assert!(part.ends_with("</capability>"));
3132 assert!(part.contains("Task Management"));
3133 }
3134
3135 #[tokio::test]
3136 async fn test_xml_tags_multiple_capabilities() {
3137 let registry = CapabilityRegistry::with_builtins();
3138 let collected = collect_capabilities(
3139 &[
3140 "stateless_todo_list".to_string(),
3141 "session_schedule".to_string(),
3142 ],
3143 ®istry,
3144 &test_ctx(),
3145 )
3146 .await;
3147
3148 assert_eq!(collected.system_prompt_parts.len(), 2);
3149 assert!(
3150 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3151 );
3152 assert!(
3153 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3154 );
3155
3156 let prefix = collected.system_prompt_prefix().unwrap();
3157 assert!(prefix.contains("</capability>\n\n<capability"));
3159 }
3160
3161 #[tokio::test]
3162 async fn test_xml_tags_system_prompt_wrapping() {
3163 let registry = CapabilityRegistry::with_builtins();
3164 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3165
3166 let applied = apply_capabilities(
3167 base,
3168 &["stateless_todo_list".to_string()],
3169 ®istry,
3170 &test_ctx(),
3171 )
3172 .await;
3173
3174 let prompt = &applied.runtime_agent.system_prompt;
3175 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3176 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3178 assert!(prompt.contains("</capability>"));
3179 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3181 }
3182
3183 #[tokio::test]
3184 async fn test_no_xml_wrapping_without_capabilities() {
3185 let registry = CapabilityRegistry::with_builtins();
3186 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3187
3188 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3189
3190 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3192 assert!(
3193 !applied
3194 .runtime_agent
3195 .system_prompt
3196 .contains("<system-prompt>")
3197 );
3198 }
3199
3200 #[tokio::test]
3201 async fn test_no_xml_wrapping_for_noop_capability() {
3202 let registry = CapabilityRegistry::with_builtins();
3203 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3204
3205 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3207
3208 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3209 assert!(
3210 !applied
3211 .runtime_agent
3212 .system_prompt
3213 .contains("<system-prompt>")
3214 );
3215 }
3216
3217 #[tokio::test]
3222 async fn test_collect_capabilities_includes_mounts() {
3223 let registry = CapabilityRegistry::with_builtins();
3224
3225 let collected =
3226 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3227
3228 assert!(!collected.mounts.is_empty());
3229 assert_eq!(collected.mounts.len(), 1);
3230 assert_eq!(collected.mounts[0].path, "/samples");
3231 assert!(collected.mounts[0].is_readonly());
3232 }
3233
3234 #[tokio::test]
3235 async fn test_collect_capabilities_empty_mounts_by_default() {
3236 let registry = CapabilityRegistry::with_builtins();
3237
3238 let collected =
3240 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3241
3242 assert!(collected.mounts.is_empty());
3243 }
3244
3245 #[tokio::test]
3246 async fn test_collect_capabilities_combines_mounts() {
3247 let registry = CapabilityRegistry::with_builtins();
3248
3249 let collected = collect_capabilities(
3252 &["sample_data".to_string(), "current_time".to_string()],
3253 ®istry,
3254 &test_ctx(),
3255 )
3256 .await;
3257
3258 assert_eq!(collected.mounts.len(), 1);
3259 assert!(
3261 collected
3262 .applied_ids
3263 .iter()
3264 .any(|id| id == "session_file_system")
3265 );
3266 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3267 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3268 }
3269
3270 #[test]
3271 fn test_sample_data_capability() {
3272 let registry = CapabilityRegistry::with_builtins();
3273 let cap = registry.get("sample_data").unwrap();
3274
3275 assert_eq!(cap.id(), "sample_data");
3276 assert_eq!(cap.name(), "Sample Data");
3277 assert_eq!(cap.status(), CapabilityStatus::Available);
3278
3279 assert!(cap.system_prompt_addition().is_some());
3281 assert!(cap.tools().is_empty());
3282
3283 assert!(!cap.mounts().is_empty());
3285 }
3286
3287 #[test]
3292 fn test_resolve_dependencies_empty() {
3293 let registry = CapabilityRegistry::with_builtins();
3294
3295 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3296
3297 assert!(resolved.resolved_ids.is_empty());
3298 assert!(resolved.added_as_dependencies.is_empty());
3299 assert!(resolved.user_selected.is_empty());
3300 }
3301
3302 #[test]
3303 fn test_resolve_dependencies_no_deps() {
3304 let registry = CapabilityRegistry::with_builtins();
3305
3306 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3308
3309 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3310 assert!(resolved.added_as_dependencies.is_empty());
3311 }
3312
3313 #[test]
3314 fn test_resolve_dependencies_with_deps() {
3315 let registry = CapabilityRegistry::with_builtins();
3316
3317 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3319
3320 assert_eq!(resolved.resolved_ids.len(), 2);
3322 let fs_pos = resolved
3323 .resolved_ids
3324 .iter()
3325 .position(|id| id == "session_file_system")
3326 .unwrap();
3327 let sd_pos = resolved
3328 .resolved_ids
3329 .iter()
3330 .position(|id| id == "sample_data")
3331 .unwrap();
3332 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3333
3334 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3336 }
3337
3338 #[test]
3339 fn test_resolve_dependencies_already_selected() {
3340 let registry = CapabilityRegistry::with_builtins();
3341
3342 let resolved = resolve_dependencies(
3344 &["session_file_system".to_string(), "sample_data".to_string()],
3345 ®istry,
3346 )
3347 .unwrap();
3348
3349 assert_eq!(resolved.resolved_ids.len(), 2);
3350 assert!(resolved.added_as_dependencies.is_empty());
3352 }
3353
3354 #[test]
3355 fn test_resolve_dependencies_preserves_order() {
3356 let registry = CapabilityRegistry::with_builtins();
3357
3358 let resolved =
3360 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
3361 .unwrap();
3362
3363 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
3364 }
3365
3366 #[test]
3367 fn test_resolve_dependencies_unknown_capability() {
3368 let registry = CapabilityRegistry::with_builtins();
3369
3370 let resolved =
3372 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
3373
3374 assert!(resolved.resolved_ids.is_empty());
3375 }
3376
3377 #[test]
3378 fn test_get_dependencies() {
3379 let registry = CapabilityRegistry::with_builtins();
3380
3381 let deps = get_dependencies("sample_data", ®istry);
3383 assert_eq!(deps, vec!["session_file_system"]);
3384
3385 let deps = get_dependencies("current_time", ®istry);
3387 assert!(deps.is_empty());
3388
3389 let deps = get_dependencies("unknown", ®istry);
3391 assert!(deps.is_empty());
3392 }
3393
3394 #[test]
3395 fn test_sample_data_has_dependency() {
3396 let registry = CapabilityRegistry::with_builtins();
3397 let cap = registry.get("sample_data").unwrap();
3398
3399 let deps = cap.dependencies();
3400 assert_eq!(deps.len(), 1);
3401 assert_eq!(deps[0], "session_file_system");
3402 }
3403
3404 #[test]
3405 fn test_noop_has_no_dependencies() {
3406 let registry = CapabilityRegistry::with_builtins();
3407 let cap = registry.get("noop").unwrap();
3408
3409 assert!(cap.dependencies().is_empty());
3410 }
3411
3412 #[test]
3416 fn test_circular_dependency_error() {
3417 struct CapA;
3419 struct CapB;
3420
3421 impl Capability for CapA {
3422 fn id(&self) -> &str {
3423 "test_cap_a"
3424 }
3425 fn name(&self) -> &str {
3426 "Test A"
3427 }
3428 fn description(&self) -> &str {
3429 "Test capability A"
3430 }
3431 fn dependencies(&self) -> Vec<&'static str> {
3432 vec!["test_cap_b"]
3433 }
3434 }
3435
3436 impl Capability for CapB {
3437 fn id(&self) -> &str {
3438 "test_cap_b"
3439 }
3440 fn name(&self) -> &str {
3441 "Test B"
3442 }
3443 fn description(&self) -> &str {
3444 "Test capability B"
3445 }
3446 fn dependencies(&self) -> Vec<&'static str> {
3447 vec!["test_cap_a"]
3448 }
3449 }
3450
3451 let mut registry = CapabilityRegistry::new();
3452 registry.register(CapA);
3453 registry.register(CapB);
3454
3455 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
3456
3457 assert!(result.is_err());
3458 match result.unwrap_err() {
3459 DependencyError::CircularDependency { capability_id, .. } => {
3460 assert_eq!(capability_id, "test_cap_a");
3461 }
3462 _ => panic!("Expected CircularDependency error"),
3463 }
3464 }
3465
3466 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
3471
3472 struct FilterTestCapability {
3474 priority: i32,
3475 }
3476
3477 impl Capability for FilterTestCapability {
3478 fn id(&self) -> &str {
3479 "filter_test"
3480 }
3481 fn name(&self) -> &str {
3482 "Filter Test"
3483 }
3484 fn description(&self) -> &str {
3485 "Test capability with message filter"
3486 }
3487 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3488 Some(Arc::new(FilterTestProvider {
3489 priority: self.priority,
3490 }))
3491 }
3492 }
3493
3494 struct FilterTestProvider {
3495 priority: i32,
3496 }
3497
3498 impl MessageFilterProvider for FilterTestProvider {
3499 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
3500 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
3502 query
3503 .filters
3504 .push(MessageFilter::Search(search.to_string()));
3505 }
3506 }
3507
3508 fn priority(&self) -> i32 {
3509 self.priority
3510 }
3511 }
3512
3513 #[tokio::test]
3514 async fn test_collect_capabilities_with_configs_no_filter_providers() {
3515 let registry = CapabilityRegistry::with_builtins();
3516 let configs = vec![AgentCapabilityConfig {
3517 capability_ref: CapabilityId::new("current_time"),
3518 config: serde_json::json!({}),
3519 }];
3520
3521 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3522
3523 assert!(collected.message_filter_providers.is_empty());
3524 assert!(!collected.has_message_filters());
3525 }
3526
3527 #[tokio::test]
3528 async fn test_collect_capabilities_with_configs_with_filter_provider() {
3529 let mut registry = CapabilityRegistry::new();
3530 registry.register(FilterTestCapability { priority: 0 });
3531
3532 let configs = vec![AgentCapabilityConfig {
3533 capability_ref: CapabilityId::new("filter_test"),
3534 config: serde_json::json!({ "search": "hello" }),
3535 }];
3536
3537 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3538
3539 assert_eq!(collected.message_filter_providers.len(), 1);
3540 assert!(collected.has_message_filters());
3541 }
3542
3543 #[tokio::test]
3544 async fn test_collect_capabilities_with_configs_filter_priority_order() {
3545 struct HighPriorityCapability;
3547 struct LowPriorityCapability;
3548
3549 impl Capability for HighPriorityCapability {
3550 fn id(&self) -> &str {
3551 "high_priority"
3552 }
3553 fn name(&self) -> &str {
3554 "High Priority"
3555 }
3556 fn description(&self) -> &str {
3557 "Test"
3558 }
3559 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3560 Some(Arc::new(FilterTestProvider { priority: 10 }))
3561 }
3562 }
3563
3564 impl Capability for LowPriorityCapability {
3565 fn id(&self) -> &str {
3566 "low_priority"
3567 }
3568 fn name(&self) -> &str {
3569 "Low Priority"
3570 }
3571 fn description(&self) -> &str {
3572 "Test"
3573 }
3574 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3575 Some(Arc::new(FilterTestProvider { priority: -5 }))
3576 }
3577 }
3578
3579 let mut registry = CapabilityRegistry::new();
3580 registry.register(HighPriorityCapability);
3581 registry.register(LowPriorityCapability);
3582
3583 let configs = vec![
3585 AgentCapabilityConfig {
3586 capability_ref: CapabilityId::new("high_priority"),
3587 config: serde_json::json!({}),
3588 },
3589 AgentCapabilityConfig {
3590 capability_ref: CapabilityId::new("low_priority"),
3591 config: serde_json::json!({}),
3592 },
3593 ];
3594
3595 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3596
3597 assert_eq!(collected.message_filter_providers.len(), 2);
3599 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
3600 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
3601 }
3602
3603 #[tokio::test]
3604 async fn test_collected_capabilities_apply_message_filters() {
3605 let mut registry = CapabilityRegistry::new();
3606 registry.register(FilterTestCapability { priority: 0 });
3607
3608 let configs = vec![AgentCapabilityConfig {
3609 capability_ref: CapabilityId::new("filter_test"),
3610 config: serde_json::json!({ "search": "test_query" }),
3611 }];
3612
3613 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3614
3615 let session_id: SessionId = Uuid::now_v7().into();
3617 let mut query = MessageQuery::new(session_id);
3618
3619 collected.apply_message_filters(&mut query);
3620
3621 assert_eq!(query.filters.len(), 1);
3623 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3624 }
3625
3626 #[tokio::test]
3627 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3628 struct SearchCapability {
3629 id: &'static str,
3630 search_term: &'static str,
3631 priority: i32,
3632 }
3633
3634 struct SearchProvider {
3635 search_term: &'static str,
3636 priority: i32,
3637 }
3638
3639 impl MessageFilterProvider for SearchProvider {
3640 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3641 query
3642 .filters
3643 .push(MessageFilter::Search(self.search_term.to_string()));
3644 }
3645
3646 fn priority(&self) -> i32 {
3647 self.priority
3648 }
3649 }
3650
3651 impl Capability for SearchCapability {
3652 fn id(&self) -> &str {
3653 self.id
3654 }
3655 fn name(&self) -> &str {
3656 "Search"
3657 }
3658 fn description(&self) -> &str {
3659 "Test"
3660 }
3661 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3662 Some(Arc::new(SearchProvider {
3663 search_term: self.search_term,
3664 priority: self.priority,
3665 }))
3666 }
3667 }
3668
3669 let mut registry = CapabilityRegistry::new();
3670 registry.register(SearchCapability {
3671 id: "cap_a",
3672 search_term: "alpha",
3673 priority: 5,
3674 });
3675 registry.register(SearchCapability {
3676 id: "cap_b",
3677 search_term: "beta",
3678 priority: 1,
3679 });
3680 registry.register(SearchCapability {
3681 id: "cap_c",
3682 search_term: "gamma",
3683 priority: 10,
3684 });
3685
3686 let configs = vec![
3687 AgentCapabilityConfig {
3688 capability_ref: CapabilityId::new("cap_a"),
3689 config: serde_json::json!({}),
3690 },
3691 AgentCapabilityConfig {
3692 capability_ref: CapabilityId::new("cap_b"),
3693 config: serde_json::json!({}),
3694 },
3695 AgentCapabilityConfig {
3696 capability_ref: CapabilityId::new("cap_c"),
3697 config: serde_json::json!({}),
3698 },
3699 ];
3700
3701 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3702
3703 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(), 3);
3710 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3711 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3712 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3713 }
3714
3715 #[test]
3716 fn test_capability_without_message_filter_returns_none() {
3717 let registry = CapabilityRegistry::with_builtins();
3718
3719 let noop = registry.get("noop").unwrap();
3720 assert!(noop.message_filter_provider().is_none());
3721
3722 let current_time = registry.get("current_time").unwrap();
3723 assert!(current_time.message_filter_provider().is_none());
3724 }
3725
3726 #[tokio::test]
3727 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3728 let mut registry = CapabilityRegistry::new();
3729 registry.register(FilterTestCapability { priority: 0 });
3730
3731 let test_config = serde_json::json!({
3732 "search": "custom_search",
3733 "extra_field": 42
3734 });
3735
3736 let configs = vec![AgentCapabilityConfig {
3737 capability_ref: CapabilityId::new("filter_test"),
3738 config: test_config.clone(),
3739 }];
3740
3741 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3742
3743 assert_eq!(collected.message_filter_providers.len(), 1);
3745 let (_, stored_config) = &collected.message_filter_providers[0];
3746 assert_eq!(*stored_config, test_config);
3747 }
3748
3749 #[test]
3754 fn test_collect_message_filters_only_collects_filters() {
3755 let mut registry = CapabilityRegistry::new();
3756 registry.register(FilterTestCapability { priority: 0 });
3757
3758 let configs = vec![AgentCapabilityConfig {
3759 capability_ref: CapabilityId::new("filter_test"),
3760 config: serde_json::json!({ "search": "test_query" }),
3761 }];
3762
3763 let collected = collect_message_filters_only(&configs, ®istry);
3764
3765 let session_id: SessionId = Uuid::now_v7().into();
3766 let mut query = MessageQuery::new(session_id);
3767 collected.apply_message_filters(&mut query);
3768
3769 assert_eq!(query.filters.len(), 1);
3770 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3771 }
3772
3773 #[test]
3774 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
3775 let base = serde_json::json!({ "context_budget_tokens": 1000 });
3776
3777 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
3779 assert_eq!(with["compaction_active"], serde_json::json!(true));
3780 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
3781
3782 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
3783 assert!(without.get("compaction_active").is_none());
3784
3785 let other = message_filter_config_for("other", &base, true);
3787 assert!(other.get("compaction_active").is_none());
3788
3789 let null_base = message_filter_config_for(
3791 INFINITY_CONTEXT_CAPABILITY_ID,
3792 &serde_json::Value::Null,
3793 true,
3794 );
3795 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
3796 }
3797
3798 #[test]
3799 fn test_infinity_context_defers_to_compaction_end_to_end() {
3800 use crate::message::Message;
3801
3802 let mut registry = CapabilityRegistry::new();
3803 registry.register(InfinityContextCapability);
3804 registry.register(CompactionCapability);
3805
3806 let tight = serde_json::json!({
3807 "context_budget_tokens": 1,
3808 "min_recent_messages": 1
3809 });
3810
3811 let solo = vec![AgentCapabilityConfig {
3813 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3814 config: tight.clone(),
3815 }];
3816 let mut messages = vec![
3817 Message::user("task"),
3818 Message::assistant("old ".repeat(400)),
3819 Message::user("recent"),
3820 ];
3821 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
3822 assert!(
3823 messages
3824 .iter()
3825 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
3826 "infinity context alone should trim and notice"
3827 );
3828
3829 let both = vec![
3831 AgentCapabilityConfig {
3832 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
3833 config: tight,
3834 },
3835 AgentCapabilityConfig {
3836 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3837 config: serde_json::json!({}),
3838 },
3839 ];
3840 let mut messages = vec![
3841 Message::user("task"),
3842 Message::assistant("old ".repeat(400)),
3843 Message::user("recent"),
3844 ];
3845 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
3846 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
3847 assert!(
3848 messages
3849 .iter()
3850 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
3851 "no hidden-history notice when compaction is the active reducer"
3852 );
3853 }
3854
3855 #[test]
3856 fn test_compaction_is_enabled_detects_compaction() {
3857 let mut registry = CapabilityRegistry::new();
3858 registry.register(CompactionCapability);
3859
3860 let with_compaction = vec![AgentCapabilityConfig {
3861 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3862 config: serde_json::json!({}),
3863 }];
3864 assert!(compaction_is_enabled(&with_compaction, ®istry));
3865
3866 let without = vec![AgentCapabilityConfig {
3867 capability_ref: CapabilityId::new("current_time"),
3868 config: serde_json::json!({}),
3869 }];
3870 assert!(!compaction_is_enabled(&without, ®istry));
3871 }
3872
3873 #[test]
3874 fn test_collect_message_filters_only_skips_unknown_capabilities() {
3875 let registry = CapabilityRegistry::new();
3876
3877 let configs = vec![AgentCapabilityConfig {
3878 capability_ref: CapabilityId::new("nonexistent"),
3879 config: serde_json::json!({}),
3880 }];
3881
3882 let collected = collect_message_filters_only(&configs, ®istry);
3883 assert!(collected.message_filter_providers.is_empty());
3884 }
3885
3886 #[test]
3887 fn test_collect_message_filters_only_preserves_priority_order() {
3888 struct PriorityFilterCap {
3889 id: &'static str,
3890 search_term: &'static str,
3891 priority: i32,
3892 }
3893
3894 struct PriorityFilterProvider {
3895 search_term: &'static str,
3896 priority: i32,
3897 }
3898
3899 impl Capability for PriorityFilterCap {
3900 fn id(&self) -> &str {
3901 self.id
3902 }
3903 fn name(&self) -> &str {
3904 self.id
3905 }
3906 fn description(&self) -> &str {
3907 "priority test"
3908 }
3909 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3910 Some(Arc::new(PriorityFilterProvider {
3911 search_term: self.search_term,
3912 priority: self.priority,
3913 }))
3914 }
3915 }
3916
3917 impl MessageFilterProvider for PriorityFilterProvider {
3918 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3919 query
3920 .filters
3921 .push(MessageFilter::Search(self.search_term.to_string()));
3922 }
3923 fn priority(&self) -> i32 {
3924 self.priority
3925 }
3926 }
3927
3928 let mut registry = CapabilityRegistry::new();
3929 registry.register(PriorityFilterCap {
3930 id: "gamma",
3931 search_term: "gamma",
3932 priority: 10,
3933 });
3934 registry.register(PriorityFilterCap {
3935 id: "alpha",
3936 search_term: "alpha",
3937 priority: 5,
3938 });
3939 registry.register(PriorityFilterCap {
3940 id: "beta",
3941 search_term: "beta",
3942 priority: 1,
3943 });
3944
3945 let configs = vec![
3946 AgentCapabilityConfig {
3947 capability_ref: CapabilityId::new("gamma"),
3948 config: serde_json::json!({}),
3949 },
3950 AgentCapabilityConfig {
3951 capability_ref: CapabilityId::new("alpha"),
3952 config: serde_json::json!({}),
3953 },
3954 AgentCapabilityConfig {
3955 capability_ref: CapabilityId::new("beta"),
3956 config: serde_json::json!({}),
3957 },
3958 ];
3959
3960 let collected = collect_message_filters_only(&configs, ®istry);
3961
3962 let session_id: SessionId = Uuid::now_v7().into();
3963 let mut query = MessageQuery::new(session_id);
3964 collected.apply_message_filters(&mut query);
3965
3966 assert_eq!(query.filters.len(), 3);
3968 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3969 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3970 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3971 }
3972
3973 #[test]
3974 fn test_collect_message_filters_only_post_load_invoked() {
3975 use crate::message::Message;
3976
3977 struct PostLoadCap;
3978 struct PostLoadProvider;
3979
3980 impl Capability for PostLoadCap {
3981 fn id(&self) -> &str {
3982 "post_load_test"
3983 }
3984 fn name(&self) -> &str {
3985 "PostLoad Test"
3986 }
3987 fn description(&self) -> &str {
3988 "test"
3989 }
3990 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3991 Some(Arc::new(PostLoadProvider))
3992 }
3993 }
3994
3995 impl MessageFilterProvider for PostLoadProvider {
3996 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
3997 fn priority(&self) -> i32 {
3998 0
3999 }
4000 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4001 messages.reverse();
4003 }
4004 }
4005
4006 let mut registry = CapabilityRegistry::new();
4007 registry.register(PostLoadCap);
4008
4009 let configs = vec![AgentCapabilityConfig {
4010 capability_ref: CapabilityId::new("post_load_test"),
4011 config: serde_json::json!({}),
4012 }];
4013
4014 let collected = collect_message_filters_only(&configs, ®istry);
4015
4016 let mut messages = vec![Message::user("first"), Message::user("second")];
4017 collected.apply_post_load_filters(&mut messages);
4018
4019 assert_eq!(messages[0].text(), Some("second"));
4021 assert_eq!(messages[1].text(), Some("first"));
4022 }
4023
4024 #[test]
4025 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4026 use crate::tool_types::ToolCall;
4027
4028 fn tool_heavy_messages() -> Vec<Message> {
4029 let mut messages = vec![Message::user("inspect files repeatedly")];
4030 for index in 0..9 {
4031 let call_id = format!("call_{index}");
4032 messages.push(Message::assistant_with_tools(
4033 "",
4034 vec![ToolCall {
4035 id: call_id.clone(),
4036 name: "read_file".to_string(),
4037 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4038 }],
4039 ));
4040 messages.push(Message::tool_result(
4041 call_id,
4042 Some(serde_json::json!({
4043 "path": "/workspace/src/lib.rs",
4044 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4045 "total_lines": 1000,
4046 "lines_shown": {"start": 1, "end": 1000},
4047 "truncated": false
4048 })),
4049 None,
4050 ));
4051 }
4052 messages
4053 }
4054
4055 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4056 messages[2]
4057 .tool_result_content()
4058 .and_then(|result| result.result.as_ref())
4059 .and_then(|result| result.get("masked"))
4060 .and_then(|masked| masked.as_bool())
4061 .unwrap_or(false)
4062 }
4063
4064 let mut registry = CapabilityRegistry::new();
4065 registry.register(CompactionCapability);
4066 let context = ModelViewContext {
4067 session_id: SessionId::new(),
4068 prior_usage: None,
4069 };
4070
4071 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4072 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4073 assert!(!first_tool_result_is_masked(&unmasked));
4074
4075 let compaction = collect_model_view_providers(
4076 &[AgentCapabilityConfig {
4077 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4078 config: serde_json::json!({}),
4079 }],
4080 ®istry,
4081 None,
4082 );
4083 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4084 assert!(first_tool_result_is_masked(&masked));
4085 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4086 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4087 }
4088
4089 struct DelegatingFilterCap {
4092 id: &'static str,
4093 inner: std::sync::Arc<InnerFilterCap>,
4094 }
4095 struct InnerFilterCap;
4096
4097 impl Capability for InnerFilterCap {
4098 fn id(&self) -> &str {
4099 "inner_filter"
4100 }
4101 fn name(&self) -> &str {
4102 "Inner Filter"
4103 }
4104 fn description(&self) -> &str {
4105 "inner"
4106 }
4107 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4108 Some(std::sync::Arc::new(SentinelFilter))
4109 }
4110 }
4111 struct SentinelFilter;
4112 impl MessageFilterProvider for SentinelFilter {
4113 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4114 }
4115 impl Capability for DelegatingFilterCap {
4116 fn id(&self) -> &str {
4117 self.id
4118 }
4119 fn name(&self) -> &str {
4120 "Delegating Filter"
4121 }
4122 fn description(&self) -> &str {
4123 "delegating"
4124 }
4125 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4126 None }
4128 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4129 Some(&*self.inner)
4130 }
4131 }
4132
4133 #[test]
4134 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4135 let inner = std::sync::Arc::new(InnerFilterCap);
4136 let outer = DelegatingFilterCap {
4137 id: "delegating_filter",
4138 inner: inner.clone(),
4139 };
4140
4141 let mut registry = CapabilityRegistry::new();
4142 registry.register(outer);
4143
4144 let configs = vec![AgentCapabilityConfig {
4145 capability_ref: CapabilityId::new("delegating_filter"),
4146 config: serde_json::json!({}),
4147 }];
4148
4149 let collected = collect_message_filters_only(&configs, ®istry);
4152 assert_eq!(
4153 collected.message_filter_providers.len(),
4154 1,
4155 "provider from resolved inner capability must be collected"
4156 );
4157 }
4158
4159 struct DelegatingMvpCap {
4160 id: &'static str,
4161 inner: std::sync::Arc<InnerMvpCap>,
4162 }
4163 struct InnerMvpCap;
4164
4165 impl Capability for InnerMvpCap {
4166 fn id(&self) -> &str {
4167 "inner_mvp"
4168 }
4169 fn name(&self) -> &str {
4170 "Inner MVP"
4171 }
4172 fn description(&self) -> &str {
4173 "inner"
4174 }
4175 fn model_view_provider(
4176 &self,
4177 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4178 struct NoopMvp;
4180 impl crate::capabilities::ModelViewProvider for NoopMvp {
4181 fn apply_model_view(
4182 &self,
4183 messages: Vec<Message>,
4184 _config: &serde_json::Value,
4185 _context: &ModelViewContext<'_>,
4186 ) -> Vec<Message> {
4187 messages
4188 }
4189 }
4190 Some(std::sync::Arc::new(NoopMvp))
4191 }
4192 }
4193 impl Capability for DelegatingMvpCap {
4194 fn id(&self) -> &str {
4195 self.id
4196 }
4197 fn name(&self) -> &str {
4198 "Delegating MVP"
4199 }
4200 fn description(&self) -> &str {
4201 "delegating"
4202 }
4203 fn model_view_provider(
4204 &self,
4205 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4206 None }
4208 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4209 Some(&*self.inner)
4210 }
4211 }
4212
4213 #[test]
4214 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4215 let inner = std::sync::Arc::new(InnerMvpCap);
4216 let outer = DelegatingMvpCap {
4217 id: "delegating_mvp",
4218 inner: inner.clone(),
4219 };
4220
4221 let mut registry = CapabilityRegistry::new();
4222 registry.register(outer);
4223
4224 let configs = vec![AgentCapabilityConfig {
4225 capability_ref: CapabilityId::new("delegating_mvp"),
4226 config: serde_json::json!({}),
4227 }];
4228
4229 let collected = collect_model_view_providers(&configs, ®istry, None);
4232 assert_eq!(
4233 collected.model_view_providers.len(),
4234 1,
4235 "provider from resolved inner capability must be collected"
4236 );
4237 }
4238
4239 #[tokio::test]
4249 async fn test_bashkit_shell_capability_produces_bash_tool() {
4250 let registry = CapabilityRegistry::with_builtins();
4251 let collected =
4252 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4253
4254 let tool_names: Vec<&str> = collected
4255 .tool_definitions
4256 .iter()
4257 .map(|t| t.name())
4258 .collect();
4259 assert!(
4260 tool_names.contains(&"bash"),
4261 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4262 tool_names
4263 );
4264 assert!(
4265 !collected.tools.is_empty(),
4266 "bashkit_shell must provide tool implementations"
4267 );
4268 }
4269
4270 #[tokio::test]
4271 async fn test_generic_harness_capability_set_produces_bash_tool() {
4272 let generic_harness_caps = vec![
4275 "session_file_system".to_string(),
4276 "bashkit_shell".to_string(),
4277 "web_fetch".to_string(),
4278 "session_storage".to_string(),
4279 "session".to_string(),
4280 "agent_instructions".to_string(),
4281 "skills".to_string(),
4282 "infinity_context".to_string(),
4283 "auto_tool_search".to_string(),
4284 ];
4285
4286 let registry = CapabilityRegistry::with_builtins();
4287 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4288
4289 let tool_names: Vec<&str> = collected
4290 .tool_definitions
4291 .iter()
4292 .map(|t| t.name())
4293 .collect();
4294 assert!(
4295 tool_names.contains(&"bash"),
4296 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4297 tool_names
4298 );
4299 }
4300
4301 #[tokio::test]
4302 async fn test_collect_capabilities_tool_count_matches_definitions() {
4303 let registry = CapabilityRegistry::with_builtins();
4306 let collected =
4307 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4308
4309 assert_eq!(
4310 collected.tools.len(),
4311 collected.tool_definitions.len(),
4312 "tool implementations ({}) must match tool definitions ({})",
4313 collected.tools.len(),
4314 collected.tool_definitions.len(),
4315 );
4316 }
4317
4318 #[tokio::test]
4322 async fn test_collect_capabilities_resolves_dependencies() {
4323 let registry = CapabilityRegistry::with_builtins();
4326 let collected =
4327 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4328
4329 assert!(
4331 collected
4332 .applied_ids
4333 .iter()
4334 .any(|id| id == "session_file_system"),
4335 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4336 collected.applied_ids
4337 );
4338
4339 let tool_names: Vec<&str> = collected
4340 .tool_definitions
4341 .iter()
4342 .map(|t| t.name())
4343 .collect();
4344
4345 assert!(
4347 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4348 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
4349 tool_names
4350 );
4351
4352 assert_eq!(
4354 collected.tools.len(),
4355 collected.tool_definitions.len(),
4356 "dependency-added tools must have implementations, not just definitions"
4357 );
4358 }
4359
4360 #[test]
4361 fn test_defaults_do_not_include_bash() {
4362 let registry = crate::ToolRegistry::with_defaults();
4365 assert!(
4366 !registry.has("bash"),
4367 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
4368 );
4369 }
4370
4371 #[tokio::test]
4378 async fn test_background_execution_auto_activates_with_bashkit_shell() {
4379 let registry = CapabilityRegistry::with_builtins();
4380 let collected =
4381 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4382
4383 let tool_names: Vec<&str> = collected
4384 .tool_definitions
4385 .iter()
4386 .map(|t| t.name())
4387 .collect();
4388 assert!(
4389 tool_names.contains(&"spawn_background"),
4390 "spawn_background must be auto-activated when bashkit_shell (a \
4391 background-capable tool) is in the agent's capability set; got: {:?}",
4392 tool_names
4393 );
4394 assert!(
4395 collected
4396 .applied_ids
4397 .iter()
4398 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4399 "background_execution must be in applied_ids when auto-activated; \
4400 got: {:?}",
4401 collected.applied_ids
4402 );
4403
4404 assert!(
4406 collected
4407 .tools
4408 .iter()
4409 .any(|t| t.name() == "spawn_background"),
4410 "spawn_background tool implementation must be present alongside the \
4411 definition (lockstep contract)"
4412 );
4413 }
4414
4415 #[tokio::test]
4418 async fn test_background_execution_does_not_auto_activate_without_hint() {
4419 let registry = CapabilityRegistry::with_builtins();
4420 let collected =
4422 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4423
4424 let tool_names: Vec<&str> = collected
4425 .tool_definitions
4426 .iter()
4427 .map(|t| t.name())
4428 .collect();
4429 assert!(
4430 !tool_names.contains(&"spawn_background"),
4431 "spawn_background must NOT be activated without a background-capable \
4432 tool; got: {:?}",
4433 tool_names
4434 );
4435 assert!(
4436 !collected
4437 .applied_ids
4438 .iter()
4439 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4440 "background_execution must not appear in applied_ids when no \
4441 background-capable tool is present; got: {:?}",
4442 collected.applied_ids
4443 );
4444 }
4445
4446 #[tokio::test]
4450 async fn test_background_execution_explicit_selection_is_idempotent() {
4451 let registry = CapabilityRegistry::with_builtins();
4452 let collected = collect_capabilities(
4453 &[
4454 "bashkit_shell".to_string(),
4455 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
4456 ],
4457 ®istry,
4458 &test_ctx(),
4459 )
4460 .await;
4461
4462 let spawn_background_count = collected
4463 .tool_definitions
4464 .iter()
4465 .filter(|t| t.name() == "spawn_background")
4466 .count();
4467 assert_eq!(
4468 spawn_background_count, 1,
4469 "spawn_background must appear exactly once even when \
4470 background_execution is selected explicitly alongside a \
4471 background-capable tool"
4472 );
4473 let applied_count = collected
4474 .applied_ids
4475 .iter()
4476 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
4477 .count();
4478 assert_eq!(
4479 applied_count, 1,
4480 "background_execution must appear exactly once in applied_ids"
4481 );
4482 }
4483
4484 #[test]
4489 fn test_defaults_do_not_include_spawn_background() {
4490 let registry = crate::ToolRegistry::with_defaults();
4491 assert!(
4492 !registry.has("spawn_background"),
4493 "with_defaults() must not include 'spawn_background' — it comes \
4494 from the background_execution capability (EVE-501)"
4495 );
4496 }
4497
4498 #[test]
4503 fn test_capability_features_default_empty() {
4504 let registry = CapabilityRegistry::with_builtins();
4505
4506 let noop = registry.get("noop").unwrap();
4508 assert!(noop.features().is_empty());
4509
4510 let current_time = registry.get("current_time").unwrap();
4511 assert!(current_time.features().is_empty());
4512 }
4513
4514 #[test]
4515 fn test_file_system_capability_features() {
4516 let registry = CapabilityRegistry::with_builtins();
4517
4518 let fs = registry.get("session_file_system").unwrap();
4519 assert_eq!(fs.features(), vec!["file_system"]);
4520 }
4521
4522 #[test]
4523 fn test_bashkit_shell_capability_features() {
4524 let registry = CapabilityRegistry::with_builtins();
4525
4526 let bash = registry.get("bashkit_shell").unwrap();
4527 assert_eq!(bash.features(), vec!["file_system"]);
4528 }
4529
4530 #[test]
4531 fn test_alias_resolves_to_canonical_capability() {
4532 let registry = CapabilityRegistry::with_builtins();
4533
4534 let via_alias = registry.get("virtual_bash").unwrap();
4536 assert_eq!(via_alias.id(), "bashkit_shell");
4537 assert!(registry.has("virtual_bash"));
4538 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
4539 assert_eq!(
4540 registry.canonical_id("bashkit_shell"),
4541 Some("bashkit_shell")
4542 );
4543 assert_eq!(registry.canonical_id("nonexistent"), None);
4544 }
4545
4546 #[test]
4547 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
4548 let registry = CapabilityRegistry::with_builtins();
4549
4550 let resolved = resolve_dependencies(
4553 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
4554 ®istry,
4555 )
4556 .unwrap();
4557 let bash_ids: Vec<_> = resolved
4558 .resolved_ids
4559 .iter()
4560 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
4561 .collect();
4562 assert_eq!(bash_ids, vec!["bashkit_shell"]);
4563 assert!(
4565 !resolved
4566 .added_as_dependencies
4567 .contains(&"bashkit_shell".to_string())
4568 );
4569 }
4570
4571 #[test]
4572 fn test_alias_preserves_explicit_config_in_resolution() {
4573 let registry = CapabilityRegistry::with_builtins();
4574
4575 let configs = vec![AgentCapabilityConfig::with_config(
4576 "virtual_bash".to_string(),
4577 serde_json::json!({"key": "value"}),
4578 )];
4579 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
4580 let bash = resolved
4581 .iter()
4582 .find(|c| c.capability_id() == "bashkit_shell")
4583 .expect("alias must resolve to canonical bashkit_shell config");
4584 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
4585 }
4586
4587 #[test]
4588 fn test_unregister_by_alias_removes_capability_and_aliases() {
4589 let mut registry = CapabilityRegistry::with_builtins();
4590
4591 assert!(registry.unregister("virtual_bash").is_some());
4592 assert!(!registry.has("bashkit_shell"));
4593 assert!(!registry.has("virtual_bash"));
4594 }
4595
4596 #[test]
4597 fn test_session_storage_capability_features() {
4598 let registry = CapabilityRegistry::with_builtins();
4599
4600 let storage = registry.get("session_storage").unwrap();
4601 let features = storage.features();
4602 assert!(features.contains(&"secrets"));
4603 assert!(features.contains(&"key_value"));
4604 }
4605
4606 #[test]
4607 fn test_session_schedule_capability_features() {
4608 let registry = CapabilityRegistry::with_builtins();
4609
4610 let schedule = registry.get("session_schedule").unwrap();
4611 assert_eq!(schedule.features(), vec!["schedules"]);
4612 }
4613
4614 #[test]
4615 fn test_session_sql_database_capability_features() {
4616 let registry = CapabilityRegistry::with_builtins();
4617
4618 let sql = registry.get("session_sql_database").unwrap();
4619 assert_eq!(sql.features(), vec!["sql_database"]);
4620 }
4621
4622 #[test]
4623 fn test_sample_data_capability_features() {
4624 let registry = CapabilityRegistry::with_builtins();
4625
4626 let sample = registry.get("sample_data").unwrap();
4627 assert_eq!(sample.features(), vec!["file_system"]);
4628 }
4629
4630 #[test]
4631 fn test_compute_features_empty() {
4632 let registry = CapabilityRegistry::with_builtins();
4633
4634 let features = compute_features(&[], ®istry);
4635 assert!(features.is_empty());
4636 }
4637
4638 #[test]
4639 fn test_compute_features_single_capability() {
4640 let registry = CapabilityRegistry::with_builtins();
4641
4642 let features = compute_features(&["session_schedule".to_string()], ®istry);
4643 assert_eq!(features, vec!["schedules"]);
4644 }
4645
4646 #[test]
4647 fn test_compute_features_multiple_capabilities() {
4648 let registry = CapabilityRegistry::with_builtins();
4649
4650 let features = compute_features(
4651 &[
4652 "session_file_system".to_string(),
4653 "session_storage".to_string(),
4654 "session_schedule".to_string(),
4655 ],
4656 ®istry,
4657 );
4658 assert!(features.contains(&"file_system".to_string()));
4659 assert!(features.contains(&"secrets".to_string()));
4660 assert!(features.contains(&"key_value".to_string()));
4661 assert!(features.contains(&"schedules".to_string()));
4662 }
4663
4664 #[test]
4665 fn test_compute_features_deduplicates() {
4666 let registry = CapabilityRegistry::with_builtins();
4667
4668 let features = compute_features(
4670 &[
4671 "session_file_system".to_string(),
4672 "bashkit_shell".to_string(),
4673 ],
4674 ®istry,
4675 );
4676 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
4677 assert_eq!(file_system_count, 1, "file_system should appear only once");
4678 }
4679
4680 #[test]
4681 fn test_compute_features_includes_dependency_features() {
4682 let registry = CapabilityRegistry::with_builtins();
4683
4684 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
4686 assert!(features.contains(&"file_system".to_string()));
4687 }
4688
4689 #[test]
4690 fn test_compute_features_generic_harness_set() {
4691 let registry = CapabilityRegistry::with_builtins();
4692
4693 let features = compute_features(
4695 &[
4696 "session_file_system".to_string(),
4697 "bashkit_shell".to_string(),
4698 "session_storage".to_string(),
4699 "session".to_string(),
4700 "session_schedule".to_string(),
4701 ],
4702 ®istry,
4703 );
4704 assert!(features.contains(&"file_system".to_string()));
4705 assert!(features.contains(&"secrets".to_string()));
4706 assert!(features.contains(&"key_value".to_string()));
4707 assert!(features.contains(&"schedules".to_string()));
4708 }
4709
4710 #[test]
4711 fn test_compute_features_unknown_capability_ignored() {
4712 let registry = CapabilityRegistry::with_builtins();
4713
4714 let features = compute_features(
4715 &["unknown_cap".to_string(), "session_schedule".to_string()],
4716 ®istry,
4717 );
4718 assert_eq!(features, vec!["schedules"]);
4719 }
4720
4721 #[test]
4722 fn test_risk_level_ordering() {
4723 assert!(RiskLevel::Low < RiskLevel::Medium);
4724 assert!(RiskLevel::Medium < RiskLevel::High);
4725 }
4726
4727 #[test]
4728 fn test_risk_level_serde_roundtrip() {
4729 let high = RiskLevel::High;
4730 let json = serde_json::to_string(&high).unwrap();
4731 assert_eq!(json, "\"high\"");
4732 let back: RiskLevel = serde_json::from_str(&json).unwrap();
4733 assert_eq!(back, RiskLevel::High);
4734 }
4735
4736 #[test]
4737 fn test_capability_risk_levels() {
4738 let registry = CapabilityRegistry::with_builtins();
4739
4740 let bash = registry.get("bashkit_shell").unwrap();
4742 assert_eq!(bash.risk_level(), RiskLevel::High);
4743
4744 let fetch = registry.get("web_fetch").unwrap();
4746 assert_eq!(fetch.risk_level(), RiskLevel::High);
4747
4748 let noop = registry.get("noop").unwrap();
4750 assert_eq!(noop.risk_level(), RiskLevel::Low);
4751 }
4752
4753 #[tokio::test]
4758 async fn test_apply_capabilities_openai_tool_search() {
4759 let registry = CapabilityRegistry::with_builtins();
4760 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4761
4762 let applied = apply_capabilities(
4763 base_runtime_agent.clone(),
4764 &["openai_tool_search".to_string()],
4765 ®istry,
4766 &test_ctx(),
4767 )
4768 .await;
4769
4770 assert_eq!(
4772 applied.runtime_agent.system_prompt,
4773 base_runtime_agent.system_prompt
4774 );
4775 assert!(applied.tool_registry.is_empty());
4776 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
4777
4778 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4780 assert!(ts.enabled);
4781 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4782 }
4783
4784 #[tokio::test]
4785 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
4786 let registry = CapabilityRegistry::with_builtins();
4787 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4788
4789 let applied = apply_capabilities(
4790 base_runtime_agent,
4791 &[
4792 "current_time".to_string(),
4793 "openai_tool_search".to_string(),
4794 "test_math".to_string(),
4795 ],
4796 ®istry,
4797 &test_ctx(),
4798 )
4799 .await;
4800
4801 assert!(applied.tool_registry.has("get_current_time"));
4803 assert!(applied.tool_registry.has("add"));
4804 assert!(applied.tool_registry.has("subtract"));
4805 assert!(applied.tool_registry.has("multiply"));
4806 assert!(applied.tool_registry.has("divide"));
4807
4808 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
4810 assert!(ts.enabled);
4811 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
4812 }
4813
4814 #[tokio::test]
4815 async fn test_collect_capabilities_tool_search_custom_threshold() {
4816 let registry = CapabilityRegistry::with_builtins();
4817
4818 let configs = vec![AgentCapabilityConfig {
4819 capability_ref: CapabilityId::new("openai_tool_search"),
4820 config: serde_json::json!({"threshold": 5}),
4821 }];
4822
4823 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4824
4825 let ts = collected.tool_search.as_ref().unwrap();
4826 assert!(ts.enabled);
4827 assert_eq!(ts.threshold, 5);
4828 }
4829
4830 #[tokio::test]
4831 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
4832 let registry = CapabilityRegistry::with_builtins();
4833
4834 let configs = vec![
4835 AgentCapabilityConfig {
4836 capability_ref: CapabilityId::new("auto_tool_search"),
4837 config: serde_json::json!({"threshold": 2}),
4838 },
4839 AgentCapabilityConfig {
4840 capability_ref: CapabilityId::new("test_math"),
4841 config: serde_json::json!({}),
4842 },
4843 ];
4844
4845 let ctx = test_ctx().with_model("claude-3-5-haiku");
4849 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4850
4851 assert!(
4852 collected.tool_search.is_none(),
4853 "auto_tool_search must not set a hosted config on a non-native model"
4854 );
4855 assert!(
4856 collected
4857 .tools
4858 .iter()
4859 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4860 "auto_tool_search must contribute the client-side tool_search tool"
4861 );
4862 assert!(
4863 !collected.tool_definition_hooks.is_empty(),
4864 "auto_tool_search must contribute a client-side deferral hook"
4865 );
4866
4867 let mut transformed = collected.tool_definitions.clone();
4868 for hook in &collected.tool_definition_hooks {
4869 transformed = hook.transform(transformed);
4870 }
4871 let add_tool = transformed
4872 .iter()
4873 .find(|tool| tool.name() == "add")
4874 .expect("test_math contributes add");
4875 assert!(
4876 add_tool.parameters().get("properties").is_none(),
4877 "generic auto_tool_search must honor the configured threshold"
4878 );
4879 }
4880
4881 #[tokio::test]
4882 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
4883 let registry = CapabilityRegistry::with_builtins();
4884
4885 let configs = vec![AgentCapabilityConfig {
4886 capability_ref: CapabilityId::new("auto_tool_search"),
4887 config: serde_json::json!({"threshold": 7}),
4888 }];
4889
4890 let ctx = test_ctx().with_model("gpt-5.4");
4893 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4894
4895 let ts = collected
4896 .tool_search
4897 .as_ref()
4898 .expect("auto_tool_search must set a hosted config on a native model");
4899 assert!(ts.enabled);
4900 assert_eq!(ts.threshold, 7);
4901 assert!(
4902 !collected
4903 .tools
4904 .iter()
4905 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4906 "hosted mechanism must not contribute the client-side tool_search tool"
4907 );
4908 assert!(
4909 collected.tool_definition_hooks.is_empty(),
4910 "hosted mechanism must not contribute a client-side deferral hook"
4911 );
4912 }
4913
4914 #[tokio::test]
4915 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
4916 let registry = CapabilityRegistry::with_builtins();
4917
4918 let configs = vec![AgentCapabilityConfig {
4919 capability_ref: CapabilityId::new("auto_tool_search"),
4920 config: serde_json::json!({"threshold": 9}),
4921 }];
4922
4923 let ctx = test_ctx().with_model("claude-opus-4-8");
4926 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
4927
4928 let ts = collected
4929 .tool_search
4930 .as_ref()
4931 .expect("auto_tool_search must set a hosted config on a native Claude model");
4932 assert!(ts.enabled);
4933 assert_eq!(ts.threshold, 9);
4934 assert!(
4935 !collected
4936 .tools
4937 .iter()
4938 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
4939 "hosted mechanism must not contribute the client-side tool_search tool"
4940 );
4941 assert!(
4942 collected.tool_definition_hooks.is_empty(),
4943 "hosted mechanism must not contribute a client-side deferral hook"
4944 );
4945 }
4946
4947 #[tokio::test]
4948 async fn test_collect_capabilities_no_tool_search_without_capability() {
4949 let registry = CapabilityRegistry::with_builtins();
4950
4951 let configs = vec![AgentCapabilityConfig {
4952 capability_ref: CapabilityId::new("current_time"),
4953 config: serde_json::json!({}),
4954 }];
4955
4956 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4957
4958 assert!(collected.tool_search.is_none());
4959 }
4960
4961 #[tokio::test]
4962 async fn test_collect_capabilities_tool_search_category_propagation() {
4963 let registry = CapabilityRegistry::with_builtins();
4964
4965 let configs = vec![
4967 AgentCapabilityConfig {
4968 capability_ref: CapabilityId::new("test_math"),
4969 config: serde_json::json!({}),
4970 },
4971 AgentCapabilityConfig {
4972 capability_ref: CapabilityId::new("openai_tool_search"),
4973 config: serde_json::json!({}),
4974 },
4975 ];
4976
4977 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4978
4979 assert!(collected.tool_search.is_some());
4981
4982 for tool_def in &collected.tool_definitions {
4984 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
4986 assert!(
4987 tool_def.category().is_some(),
4988 "Tool {} should have a category from its capability",
4989 tool_def.name()
4990 );
4991 }
4992 }
4993 }
4994
4995 #[tokio::test]
4996 async fn test_apply_capabilities_prompt_caching() {
4997 let registry = CapabilityRegistry::with_builtins();
4998 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
4999
5000 let applied = apply_capabilities(
5001 base_runtime_agent.clone(),
5002 &["prompt_caching".to_string()],
5003 ®istry,
5004 &test_ctx(),
5005 )
5006 .await;
5007
5008 assert_eq!(
5009 applied.runtime_agent.system_prompt,
5010 base_runtime_agent.system_prompt
5011 );
5012 assert!(applied.tool_registry.is_empty());
5013 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
5014
5015 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
5016 assert!(prompt_cache.enabled);
5017 assert_eq!(
5018 prompt_cache.strategy,
5019 crate::driver_registry::PromptCacheStrategy::Auto
5020 );
5021 assert!(prompt_cache.gemini_cached_content.is_none());
5022 }
5023
5024 #[tokio::test]
5025 async fn test_apply_capabilities_openrouter_server_tools() {
5026 let registry = CapabilityRegistry::with_builtins();
5027 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5028
5029 let configs = vec![AgentCapabilityConfig {
5030 capability_ref: CapabilityId::new("openrouter_server_tools"),
5031 config: serde_json::json!({
5032 "tools": ["web_search", "datetime"],
5033 "web_search_max_results": 4,
5034 }),
5035 }];
5036
5037 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5038 let routing = collected
5039 .openrouter_routing
5040 .as_ref()
5041 .expect("server tools produce routing config");
5042 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
5043 assert_eq!(
5044 kinds,
5045 vec![
5046 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
5047 crate::driver_registry::OpenRouterServerToolKind::Datetime,
5048 ]
5049 );
5050
5051 let applied = apply_capabilities(
5054 base_runtime_agent,
5055 &["openrouter_server_tools".to_string()],
5056 ®istry,
5057 &test_ctx(),
5058 )
5059 .await;
5060 assert!(applied.tool_registry.is_empty());
5061 assert!(applied.runtime_agent.openrouter_routing.is_none());
5062 }
5063
5064 #[tokio::test]
5065 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
5066 let registry = CapabilityRegistry::with_builtins();
5067
5068 let configs = vec![AgentCapabilityConfig {
5069 capability_ref: CapabilityId::new("prompt_caching"),
5070 config: serde_json::json!({"strategy": "auto"}),
5071 }];
5072
5073 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5074
5075 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5076 assert!(prompt_cache.enabled);
5077 assert_eq!(
5078 prompt_cache.strategy,
5079 crate::driver_registry::PromptCacheStrategy::Auto
5080 );
5081 assert!(prompt_cache.gemini_cached_content.is_none());
5082 }
5083
5084 #[tokio::test]
5085 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
5086 let registry = CapabilityRegistry::with_builtins();
5087
5088 let configs = vec![AgentCapabilityConfig {
5089 capability_ref: CapabilityId::new("prompt_caching"),
5090 config: serde_json::json!({
5091 "strategy": "auto",
5092 "gemini_cached_content": "cachedContents/demo-cache"
5093 }),
5094 }];
5095
5096 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5097
5098 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5099 assert_eq!(
5100 prompt_cache.gemini_cached_content.as_deref(),
5101 Some("cachedContents/demo-cache")
5102 );
5103 }
5104
5105 #[tokio::test]
5106 async fn test_collect_capabilities_parallel_tool_calls_modes() {
5107 let registry = CapabilityRegistry::with_builtins();
5108
5109 let collected = collect_capabilities_with_configs(
5111 &[AgentCapabilityConfig::new("parallel_tool_calls")],
5112 ®istry,
5113 &test_ctx(),
5114 )
5115 .await;
5116 assert_eq!(collected.parallel_tool_calls, Some(true));
5117
5118 let collected = collect_capabilities_with_configs(
5120 &[AgentCapabilityConfig {
5121 capability_ref: CapabilityId::new("parallel_tool_calls"),
5122 config: serde_json::json!({"mode": "avoid"}),
5123 }],
5124 ®istry,
5125 &test_ctx(),
5126 )
5127 .await;
5128 assert_eq!(collected.parallel_tool_calls, Some(false));
5129
5130 let collected = collect_capabilities_with_configs(
5132 &[AgentCapabilityConfig {
5133 capability_ref: CapabilityId::new("parallel_tool_calls"),
5134 config: serde_json::json!({"mode": "none"}),
5135 }],
5136 ®istry,
5137 &test_ctx(),
5138 )
5139 .await;
5140 assert_eq!(collected.parallel_tool_calls, None);
5141
5142 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
5144 assert_eq!(collected.parallel_tool_calls, None);
5145 }
5146
5147 #[tokio::test]
5148 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
5149 let registry = CapabilityRegistry::with_builtins();
5150
5151 let applied = apply_capabilities(
5153 RuntimeAgent::new("p", "gpt-5.2"),
5154 &["parallel_tool_calls".to_string()],
5155 ®istry,
5156 &test_ctx(),
5157 )
5158 .await;
5159 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
5160
5161 let mut base = RuntimeAgent::new("p", "gpt-5.2");
5163 base.parallel_tool_calls = Some(false);
5164 let applied = apply_capabilities(
5165 base,
5166 &["parallel_tool_calls".to_string()],
5167 ®istry,
5168 &test_ctx(),
5169 )
5170 .await;
5171 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
5172 }
5173
5174 struct SkillContributingCapability;
5179
5180 impl Capability for SkillContributingCapability {
5181 fn id(&self) -> &str {
5182 "contributes_skills"
5183 }
5184 fn name(&self) -> &str {
5185 "Contributes Skills"
5186 }
5187 fn description(&self) -> &str {
5188 "Test capability that contributes skills."
5189 }
5190 fn contribute_skills(&self) -> Vec<SkillContribution> {
5191 vec![
5192 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
5193 .with_files(vec![(
5194 "scripts/a.sh".to_string(),
5195 "#!/bin/sh\necho a\n".to_string(),
5196 )]),
5197 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
5198 .with_user_invocable(false),
5199 ]
5200 }
5201 }
5202
5203 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
5204 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
5205 MountSource::InlineFile { content, .. } => content.as_str(),
5206 _ => panic!("Expected InlineFile for SKILL.md"),
5207 }
5208 }
5209
5210 #[tokio::test]
5211 async fn test_contribute_skills_normalized_to_mounts() {
5212 let mut registry = CapabilityRegistry::new();
5213 registry.register(SkillContributingCapability);
5214
5215 let configs = vec![AgentCapabilityConfig {
5216 capability_ref: CapabilityId::new("contributes_skills"),
5217 config: serde_json::json!({}),
5218 }];
5219
5220 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5221
5222 let skill_mounts: Vec<_> = collected
5223 .mounts
5224 .iter()
5225 .filter(|m| m.path.starts_with("/.agents/skills/"))
5226 .collect();
5227 assert_eq!(skill_mounts.len(), 2);
5228
5229 for m in &skill_mounts {
5232 assert!(m.is_readonly());
5233 assert_eq!(m.capability_id, "contributes_skills");
5234 }
5235
5236 let alpha = skill_mounts
5237 .iter()
5238 .find(|m| m.path == "/.agents/skills/alpha-skill")
5239 .expect("alpha-skill mount missing");
5240 match &alpha.source {
5241 MountSource::InlineDirectory { entries } => {
5242 assert!(entries.contains_key("SKILL.md"));
5243 assert!(entries.contains_key("scripts/a.sh"));
5244 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5245 assert_eq!(parsed.name, "alpha-skill");
5246 assert!(parsed.user_invocable);
5247 }
5248 _ => panic!("Expected InlineDirectory"),
5249 }
5250
5251 let beta = skill_mounts
5252 .iter()
5253 .find(|m| m.path == "/.agents/skills/beta-skill")
5254 .expect("beta-skill mount missing");
5255 match &beta.source {
5256 MountSource::InlineDirectory { entries } => {
5257 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5258 assert!(!parsed.user_invocable);
5259 }
5260 _ => panic!("Expected InlineDirectory"),
5261 }
5262 }
5263
5264 #[tokio::test]
5265 async fn test_contribute_skills_default_empty() {
5266 let mut registry = CapabilityRegistry::new();
5269 registry.register(FilterTestCapability { priority: 0 });
5270
5271 let configs = vec![AgentCapabilityConfig {
5272 capability_ref: CapabilityId::new("filter_test"),
5273 config: serde_json::json!({}),
5274 }];
5275
5276 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5277 assert!(
5278 collected
5279 .mounts
5280 .iter()
5281 .all(|m| !m.path.starts_with("/.agents/skills/"))
5282 );
5283 }
5284
5285 struct LocalizedCapability;
5286
5287 impl Capability for LocalizedCapability {
5288 fn id(&self) -> &str {
5289 "localized"
5290 }
5291 fn name(&self) -> &str {
5292 "Localized"
5293 }
5294 fn description(&self) -> &str {
5295 "English description"
5296 }
5297 fn localizations(&self) -> Vec<CapabilityLocalization> {
5298 vec![
5299 CapabilityLocalization {
5300 locale: "en",
5301 name: None,
5302 description: None,
5303 config_description: Some("Controls things."),
5304 config_overlay: None,
5305 },
5306 CapabilityLocalization {
5307 locale: "uk",
5308 name: Some("Локалізована"),
5309 description: Some("Український опис"),
5310 config_description: Some("Керує налаштуваннями."),
5311 config_overlay: None,
5312 },
5313 ]
5314 }
5315 }
5316
5317 #[test]
5318 fn localized_name_falls_back_exact_language_then_base() {
5319 let cap = LocalizedCapability;
5320 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
5322 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
5323 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
5325 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
5327 assert_eq!(cap.localized_name(None), "Localized");
5328 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
5329 assert_eq!(cap.localized_description(Some("de")), "English description");
5330 }
5331
5332 #[test]
5333 fn describe_schema_resolves_config_description_per_locale() {
5334 let cap = LocalizedCapability;
5335 assert_eq!(
5336 cap.describe_schema(Some("uk-UA")).as_deref(),
5337 Some("Керує налаштуваннями.")
5338 );
5339 assert_eq!(
5341 cap.describe_schema(Some("pl")).as_deref(),
5342 Some("Controls things.")
5343 );
5344 assert_eq!(
5345 cap.describe_schema(None).as_deref(),
5346 Some("Controls things.")
5347 );
5348 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
5350 }
5351}