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;
103pub mod facts;
104mod fake_aws;
105mod fake_crm;
106mod fake_financial;
107mod fake_warehouse;
108mod file_system;
109mod guardrails;
110mod human_intent;
111mod infinity_context;
112mod knowledge_base;
113mod knowledge_index;
114mod loop_detection;
115mod lua;
116mod lua_code_mode;
117pub mod mcp;
118mod memory;
119mod message_metadata;
120mod model_scout;
121mod monitors;
122mod noop;
123mod openai_tool_search;
124mod openrouter_server_tools;
125mod openrouter_workspace;
126#[cfg(feature = "ui-capabilities")]
127mod openui;
128mod parallel_tool_calls;
129mod platform_management;
130mod prompt_caching;
131mod prompt_canary_guardrail;
132mod research;
133mod sample_data;
134mod self_budget;
135mod session;
136mod session_sandbox;
137mod session_schedule;
138mod session_sql_database;
139mod session_storage;
140mod session_tasks;
141mod skills;
142mod skills_scoped;
143mod stateless_todo_list;
144mod subagents;
145mod system_commands;
146mod test_math;
147mod test_weather;
148mod tool_call_repair;
149mod tool_output_distillation;
150mod tool_output_persistence;
151mod tool_search;
152pub mod user_hooks;
153mod util;
154#[cfg(feature = "web-fetch")]
155mod web_fetch;
156
157pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
162pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
166#[cfg(feature = "a2a")]
167pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
168#[cfg(feature = "ui-capabilities")]
169pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
170pub use agent_handoff::{
171 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
172 MessageAgentHandoffTool, StartAgentHandoffTool,
173};
174pub use agent_instructions::{
175 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
176 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
177 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
178};
179pub use attach_skill::{
180 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
181 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
182 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
183};
184pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
185pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
186pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
187pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
188pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
189pub use compaction::{
190 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
191 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
192 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
193 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
194 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
195 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
196 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
197};
198pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
199pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
200pub use declarative::{
201 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
202 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
203 declarative_capability_info, hydrate_declarative_capability_config,
204 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
205 plugin_capability_info, validate_declarative_capability_definition,
206};
207pub use error_disclosure::{
208 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
209};
210pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
211pub use fake_aws::{
212 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
213 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
214 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
215 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
216};
217pub use fake_crm::{
218 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
219 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
220 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
221};
222pub use fake_financial::{
223 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
224 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
225 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
226 FinanceListTransactionsTool,
227};
228pub use fake_warehouse::{
229 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
230 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
231 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
232 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
233};
234pub use file_system::{
235 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
236 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
237};
238pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
239pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
240pub use infinity_context::{
241 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
242};
243pub use knowledge_base::{
244 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
245 validate_knowledge_base_config,
246};
247pub use knowledge_index::{
248 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
249 validate_knowledge_index_config,
250};
251pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
252pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
253pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
254pub use mcp::{
255 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
256 parse_mcp_capability_id,
257};
258pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
259pub use message_metadata::{
260 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
261 MessageMetadataField, render_annotation,
262};
263pub use model_scout::{
264 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
265 RouterUpdateProposal, compute_score, rank_results,
266};
267pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
268pub use openai_tool_search::{
269 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
270 model_supports_native_tool_search,
271};
272pub use openrouter_server_tools::{
273 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
274};
275pub use openrouter_workspace::{
276 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
277 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
278 detect_policy_drift,
279};
280#[cfg(feature = "ui-capabilities")]
281pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
282pub use parallel_tool_calls::{
283 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
284 parallel_tool_calls_from_config,
285};
286pub use platform_management::{
287 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
288 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
289 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
290};
291pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
292pub use prompt_canary_guardrail::{
293 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
294 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
295 REASON_CODE_SYSTEM_PROMPT_LEAK,
296};
297pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
298pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
299pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
300pub use session::{
301 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
302};
303pub use session_sandbox::{
304 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
305 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
306};
307pub use session_schedule::{
308 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
309 SessionScheduleCapability,
310};
311pub use session_sql_database::{
312 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
313 SqlSchemaTool,
314};
315pub use session_storage::{
316 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
317 is_internal_session_kv_key,
318};
319pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
320pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
321pub use skills_scoped::{
322 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
323};
324pub use stateless_todo_list::{
325 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
326};
327pub use subagents::{SUBAGENTS_CAPABILITY_ID, SubagentCapability};
328pub use bashkit_shell::{
330 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
331};
332pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
333pub use test_math::{
334 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
335};
336pub use test_weather::{
337 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
338};
339pub use tool_call_repair::{
340 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
341 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
342 salvage_tool_arguments, tool_call_repair_capability,
343};
344pub use tool_output_distillation::{
345 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
346};
347pub use tool_output_persistence::{
348 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
349};
350pub use tool_search::{
351 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
352};
353pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
354#[cfg(feature = "web-fetch")]
355pub use web_fetch::{
356 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
357 derive_bot_auth_public_key,
358};
359
360pub struct SystemPromptContext {
370 pub session_id: SessionId,
372 pub locale: Option<String>,
374 pub file_store: Option<Arc<dyn SessionFileSystem>>,
376 pub model: Option<String>,
382}
383
384impl SystemPromptContext {
385 pub fn without_file_store(session_id: SessionId) -> Self {
387 Self {
388 session_id,
389 locale: None,
390 file_store: None,
391 model: None,
392 }
393 }
394
395 pub fn with_model(mut self, model: impl Into<String>) -> Self {
397 self.model = Some(model.into());
398 self
399 }
400}
401
402#[derive(Debug, Clone)]
454pub struct CapabilityLocalization {
455 pub locale: &'static str,
457 pub name: Option<&'static str>,
459 pub description: Option<&'static str>,
461 pub config_description: Option<&'static str>,
466 pub config_overlay: Option<serde_json::Value>,
472}
473
474impl CapabilityLocalization {
475 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
477 Self {
478 locale,
479 name: Some(name),
480 description: Some(description),
481 config_description: None,
482 config_overlay: None,
483 }
484 }
485}
486
487pub fn resolve_localized_field<T>(
491 localizations: &[CapabilityLocalization],
492 locale: Option<&str>,
493 field: impl Fn(&CapabilityLocalization) -> Option<T>,
494) -> Option<T> {
495 let mut candidates: Vec<String> = Vec::new();
496 if let Some(raw) = locale {
497 let normalized = raw.trim().replace('_', "-").to_lowercase();
498 if !normalized.is_empty() {
499 if let Some((language, _)) = normalized.split_once('-') {
500 let language = language.to_string();
501 candidates.push(normalized);
502 candidates.push(language);
503 } else {
504 candidates.push(normalized);
505 }
506 }
507 }
508 candidates.push("en".to_string());
509
510 for candidate in candidates {
511 let hit = localizations
512 .iter()
513 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
514 .and_then(&field);
515 if hit.is_some() {
516 return hit;
517 }
518 }
519 None
520}
521
522#[async_trait]
523pub trait Capability: Send + Sync {
524 fn id(&self) -> &str;
526
527 fn aliases(&self) -> Vec<&'static str> {
536 vec![]
537 }
538
539 fn name(&self) -> &str;
541
542 fn description(&self) -> &str;
544
545 fn localizations(&self) -> Vec<CapabilityLocalization> {
550 vec![]
551 }
552
553 fn localized_name(&self, locale: Option<&str>) -> String {
556 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
557 .unwrap_or_else(|| self.name())
558 .to_string()
559 }
560
561 fn localized_description(&self, locale: Option<&str>) -> String {
563 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
564 .unwrap_or_else(|| self.description())
565 .to_string()
566 }
567
568 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
572 resolve_localized_field(&self.localizations(), locale, |entry| {
573 entry.config_description
574 })
575 .map(str::to_string)
576 }
577
578 fn status(&self) -> CapabilityStatus {
580 CapabilityStatus::Available
581 }
582
583 fn icon(&self) -> Option<&str> {
585 None
586 }
587
588 fn category(&self) -> Option<&str> {
590 None
591 }
592
593 fn is_guardrail(&self) -> bool {
598 false
599 }
600
601 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
612 None
613 }
614
615 fn system_prompt_addition(&self) -> Option<&str> {
635 None
636 }
637
638 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
650 self.system_prompt_addition().map(|addition| {
651 format!(
652 "<capability id=\"{}\">\n{}\n</capability>",
653 self.id(),
654 addition
655 )
656 })
657 }
658
659 fn system_prompt_preview(&self) -> Option<String> {
665 self.system_prompt_addition().map(|s| s.to_string())
666 }
667
668 fn tools(&self) -> Vec<Box<dyn Tool>> {
670 vec![]
671 }
672
673 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
681 self.tools()
682 }
683
684 async fn system_prompt_contribution_with_config(
691 &self,
692 ctx: &SystemPromptContext,
693 _config: &serde_json::Value,
694 ) -> Option<String> {
695 self.system_prompt_contribution(ctx).await
696 }
697
698 fn tool_definitions(&self) -> Vec<ToolDefinition> {
701 self.tools().iter().map(|t| t.to_definition()).collect()
702 }
703
704 fn mounts(&self) -> Vec<MountPoint> {
712 vec![]
713 }
714
715 fn dependencies(&self) -> Vec<&'static str> {
724 vec![]
725 }
726
727 fn features(&self) -> Vec<&'static str> {
742 vec![]
743 }
744
745 fn config_schema(&self) -> Option<serde_json::Value> {
751 None
752 }
753
754 fn config_ui_schema(&self) -> Option<serde_json::Value> {
759 None
760 }
761
762 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
768 Ok(())
769 }
770
771 fn mcp_servers(&self) -> ScopedMcpServers {
777 ScopedMcpServers::default()
778 }
779
780 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
782 self.mcp_servers()
783 }
784
785 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
798 None
799 }
800
801 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
809 None
810 }
811
812 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
827 vec![]
828 }
829
830 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
841 vec![]
842 }
843
844 fn pre_tool_use_hooks_with_config(
849 &self,
850 _config: &serde_json::Value,
851 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
852 self.pre_tool_use_hooks()
853 }
854
855 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
863 vec![]
864 }
865
866 fn post_tool_exec_hooks_with_config(
871 &self,
872 _config: &serde_json::Value,
873 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
874 self.post_tool_exec_hooks()
875 }
876
877 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
886 vec![]
887 }
888
889 fn tool_definition_hooks_with_config(
894 &self,
895 _config: &serde_json::Value,
896 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
897 self.tool_definition_hooks()
898 }
899
900 fn tool_definition_hooks_with_context(
910 &self,
911 _ctx: &SystemPromptContext,
912 config: &serde_json::Value,
913 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
914 self.tool_definition_hooks_with_config(config)
915 }
916
917 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
925 vec![]
926 }
927
928 fn narrate(
942 &self,
943 _tool_def: Option<&ToolDefinition>,
944 tool_call: &ToolCall,
945 phase: crate::tool_narration::ToolNarrationPhase,
946 locale: Option<&str>,
947 ) -> Option<String> {
948 self.tools()
949 .iter()
950 .find(|tool| tool.name() == tool_call.name)
951 .and_then(|tool| tool.narrate(tool_call, phase, locale))
952 }
953
954 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
970 vec![]
971 }
972
973 fn user_hooks_with_config(
979 &self,
980 _config: &serde_json::Value,
981 ) -> Vec<crate::user_hook_types::UserHookSpec> {
982 self.user_hooks()
983 }
984
985 fn risk_level(&self) -> RiskLevel {
993 RiskLevel::Low
994 }
995
996 fn commands(&self) -> Vec<CommandDescriptor> {
1004 vec![]
1005 }
1006
1007 async fn execute_command(
1021 &self,
1022 request: &ExecuteCommandRequest,
1023 _ctx: &CommandExecutionContext,
1024 ) -> crate::error::Result<CommandResult> {
1025 Err(crate::error::AgentLoopError::config(format!(
1026 "capability {} declared command /{} but does not implement execute_command",
1027 self.id(),
1028 request.name,
1029 )))
1030 }
1031
1032 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1040 vec![]
1041 }
1042
1043 fn contribute_skills(&self) -> Vec<SkillContribution> {
1053 vec![]
1054 }
1055
1056 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1067 vec![]
1068 }
1069
1070 fn post_output_guardrails_with_config(
1082 &self,
1083 _config: &serde_json::Value,
1084 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1085 vec![]
1086 }
1087}
1088
1089pub trait ToolDefinitionHook: Send + Sync {
1090 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1091
1092 fn applies_with_native_tool_search(&self) -> bool {
1097 true
1098 }
1099}
1100
1101pub trait ToolCallHook: Send + Sync {
1102 fn narration(
1103 &self,
1104 _tool_def: Option<&ToolDefinition>,
1105 _tool_call: &ToolCall,
1106 _phase: crate::tool_narration::ToolNarrationPhase,
1107 _locale: Option<&str>,
1108 ) -> Option<String> {
1109 None
1110 }
1111
1112 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1113 tool_call
1114 }
1115}
1116
1117pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1123
1124impl ToolCallHook for CapabilityNarrationHook {
1125 fn narration(
1126 &self,
1127 tool_def: Option<&ToolDefinition>,
1128 tool_call: &ToolCall,
1129 phase: crate::tool_narration::ToolNarrationPhase,
1130 locale: Option<&str>,
1131 ) -> Option<String> {
1132 self.0.narrate(tool_def, tool_call, phase, locale)
1133 }
1134}
1135
1136#[derive(
1140 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1141)]
1142#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1143#[cfg_attr(feature = "openapi", schema(example = "low"))]
1144#[serde(rename_all = "lowercase")]
1145pub enum RiskLevel {
1146 Low,
1148 Medium,
1150 High,
1152}
1153
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1160#[serde(rename_all = "snake_case")]
1161pub enum BlueprintModel {
1162 Fixed(String),
1164 Default(String),
1166 Inherit,
1168}
1169
1170pub struct AgentBlueprint {
1176 pub id: &'static str,
1178 pub name: &'static str,
1180 pub description: &'static str,
1182 pub model: BlueprintModel,
1184 pub system_prompt: &'static str,
1186 pub tools: Vec<Box<dyn Tool>>,
1188 pub max_turns: Option<usize>,
1190 pub config_schema: Option<serde_json::Value>,
1192}
1193
1194impl AgentBlueprint {
1195 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1197 self.tools.iter().map(|t| t.to_definition()).collect()
1198 }
1199}
1200
1201impl std::fmt::Debug for AgentBlueprint {
1202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1203 f.debug_struct("AgentBlueprint")
1204 .field("id", &self.id)
1205 .field("name", &self.name)
1206 .field("model", &self.model)
1207 .field("tool_count", &self.tools.len())
1208 .field("max_turns", &self.max_turns)
1209 .finish()
1210 }
1211}
1212
1213#[derive(Clone)]
1240pub struct CapabilityRegistry {
1241 capabilities: HashMap<String, Arc<dyn Capability>>,
1242 aliases: HashMap<String, String>,
1244}
1245
1246impl CapabilityRegistry {
1247 pub fn new() -> Self {
1249 Self {
1250 capabilities: HashMap::new(),
1251 aliases: HashMap::new(),
1252 }
1253 }
1254
1255 pub fn with_builtins() -> Self {
1260 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1261 }
1262
1263 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1268 let mut registry = Self::new();
1269
1270 registry.register(AgentInstructionsCapability);
1272 registry.register(HumanIntentCapability);
1273 registry.register(NoopCapability);
1274 registry.register(CurrentTimeCapability);
1275 registry.register(MessageMetadataCapability);
1276 registry.register(ResearchCapability);
1277 registry.register(ModelScoutCapability);
1278 registry.register(OpenRouterWorkspaceCapability);
1279 registry.register(OpenRouterServerToolsCapability);
1280 registry.register(PlatformManagementCapability);
1281 registry.register(FileSystemCapability);
1282 registry.register(MemoryCapability);
1283 registry.register(SessionStorageCapability);
1284 registry.register(SessionCapability);
1285 registry.register(SessionSqlDatabaseCapability);
1286 registry.register(TestMathCapability);
1287 registry.register(TestWeatherCapability);
1288 registry.register(StatelessTodoListCapability);
1289 #[cfg(feature = "web-fetch")]
1290 registry.register(WebFetchCapability::from_env());
1291 registry.register(BashkitShellCapability);
1292 registry.register(BackgroundExecutionCapability);
1293 registry.register(SessionScheduleCapability);
1294 registry.register(BtwCapability);
1295 registry.register(InfinityContextCapability);
1296 registry.register(budgeting::BudgetingCapability);
1297 registry.register(SelfBudgetCapability);
1298 registry.register(CompactionCapability);
1299 registry.register(ErrorDisclosureCapability);
1300
1301 registry.register(OpenAiToolSearchCapability::new());
1303 registry.register(ClaudeToolSearchCapability::new());
1305 registry.register(ToolSearchCapability::new());
1307 registry.register(AutoToolSearchCapability::new());
1309 registry.register(PromptCachingCapability::new());
1310
1311 registry.register(ParallelToolCallsCapability);
1313
1314 registry.register(SkillsCapability);
1316
1317 registry.register(SubagentCapability);
1319
1320 registry.register(SessionTasksCapability);
1322
1323 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1327 registry.register(AgentHandoffCapability);
1328 #[cfg(feature = "a2a")]
1332 registry.register(A2aAgentDelegationCapability);
1333 }
1334
1335 registry.register(SystemCommandsCapability);
1337
1338 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1340 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1341
1342 registry.register(user_hooks::UserHooksCapability);
1345
1346 registry.register(LoopDetectionCapability);
1348
1349 registry.register(ToolCallRepairCapability);
1353
1354 registry.register(PromptCanaryGuardrailCapability);
1357
1358 registry.register(GuardrailsCapability);
1361
1362 #[cfg(feature = "ui-capabilities")]
1364 {
1365 registry.register(OpenUiCapability);
1366 registry.register(A2UiCapability);
1367 }
1368
1369 registry.register(SampleDataCapability);
1371
1372 registry.register(DataKnowledgeCapability);
1374
1375 registry.register(KnowledgeBaseCapability);
1377
1378 registry.register(KnowledgeIndexCapability);
1380
1381 registry.register(FakeWarehouseCapability);
1383 registry.register(FakeAwsCapability);
1384 registry.register(FakeCrmCapability);
1385 registry.register(FakeFinancialCapability);
1386
1387 let internal_flags = crate::InternalFeatureFlags::from_env();
1389 if internal_flags.session_sandbox {
1390 registry.register(SessionSandboxCapability);
1391 }
1392
1393 if internal_flags.lua {
1397 registry.register(LuaCapability);
1398 registry.register(LuaCodeModeCapability);
1401 }
1402 for plugin in inventory::iter::<IntegrationPlugin>() {
1403 if (!plugin.experimental_only || grade.experimental_features_enabled())
1404 && plugin
1405 .feature_flag
1406 .is_none_or(|f| internal_flags.is_enabled(f))
1407 {
1408 registry.register_boxed((plugin.factory)());
1409 }
1410 }
1411
1412 registry
1413 }
1414
1415 pub fn register(&mut self, capability: impl Capability + 'static) {
1417 self.register_arc(Arc::new(capability));
1418 }
1419
1420 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1422 self.register_arc(Arc::from(capability));
1423 }
1424
1425 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1427 let canonical = capability.id().to_string();
1428 for alias in capability.aliases() {
1429 self.aliases.insert(alias.to_string(), canonical.clone());
1430 }
1431 self.capabilities.insert(canonical, capability);
1432 }
1433
1434 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1436 self.capabilities
1437 .get(id)
1438 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1439 }
1440
1441 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1446 if self.capabilities.contains_key(id) {
1447 Some(id)
1448 } else {
1449 self.aliases
1450 .get(id)
1451 .filter(|c| self.capabilities.contains_key(*c))
1452 .map(String::as_str)
1453 }
1454 }
1455
1456 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1458 let canonical = self.canonical_id(id)?.to_string();
1459 let removed = self.capabilities.remove(&canonical);
1460 self.aliases.retain(|_, target| *target != canonical);
1461 removed
1462 }
1463
1464 pub fn has(&self, id: &str) -> bool {
1466 self.get(id).is_some()
1467 }
1468
1469 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1471 self.capabilities.values().collect()
1472 }
1473
1474 pub fn len(&self) -> usize {
1476 self.capabilities.len()
1477 }
1478
1479 pub fn is_empty(&self) -> bool {
1481 self.capabilities.is_empty()
1482 }
1483
1484 pub fn builder() -> CapabilityRegistryBuilder {
1486 CapabilityRegistryBuilder::new()
1487 }
1488
1489 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1493 for cap in self.capabilities.values() {
1494 for bp in cap.agent_blueprints() {
1495 if bp.id == id {
1496 return Some(bp);
1497 }
1498 }
1499 }
1500 None
1501 }
1502
1503 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1507 for (capability_id, cap) in &self.capabilities {
1508 for bp in cap.agent_blueprints() {
1509 if bp.id == id {
1510 return Some((capability_id.clone(), bp));
1511 }
1512 }
1513 }
1514 None
1515 }
1516
1517 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1519 self.capabilities
1520 .values()
1521 .flat_map(|cap| cap.agent_blueprints())
1522 .collect()
1523 }
1524}
1525
1526impl Default for CapabilityRegistry {
1527 fn default() -> Self {
1528 Self::with_builtins()
1529 }
1530}
1531
1532impl std::fmt::Debug for CapabilityRegistry {
1533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1534 let ids: Vec<_> = self.capabilities.keys().collect();
1535 f.debug_struct("CapabilityRegistry")
1536 .field("capabilities", &ids)
1537 .finish()
1538 }
1539}
1540
1541pub struct CapabilityRegistryBuilder {
1543 registry: CapabilityRegistry,
1544}
1545
1546impl CapabilityRegistryBuilder {
1547 pub fn new() -> Self {
1549 Self {
1550 registry: CapabilityRegistry::new(),
1551 }
1552 }
1553
1554 pub fn with_builtins() -> Self {
1556 Self {
1557 registry: CapabilityRegistry::with_builtins(),
1558 }
1559 }
1560
1561 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1563 self.registry.register(capability);
1564 self
1565 }
1566
1567 pub fn build(self) -> CapabilityRegistry {
1569 self.registry
1570 }
1571}
1572
1573impl Default for CapabilityRegistryBuilder {
1574 fn default() -> Self {
1575 Self::new()
1576 }
1577}
1578
1579pub struct ModelViewContext<'a> {
1585 pub session_id: SessionId,
1586 pub prior_usage: Option<&'a TokenUsage>,
1587}
1588
1589pub trait ModelViewProvider: Send + Sync {
1595 fn apply_model_view(
1596 &self,
1597 messages: Vec<Message>,
1598 config: &serde_json::Value,
1599 context: &ModelViewContext<'_>,
1600 ) -> Vec<Message>;
1601
1602 fn priority(&self) -> i32 {
1603 0
1604 }
1605}
1606
1607pub struct CollectedCapabilities {
1612 pub system_prompt_parts: Vec<String>,
1614 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1616 pub tools: Vec<Box<dyn Tool>>,
1618 pub tool_definitions: Vec<ToolDefinition>,
1620 pub mounts: Vec<MountPoint>,
1622 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1624 pub applied_ids: Vec<String>,
1626 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1628 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1630 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1633 pub parallel_tool_calls: Option<bool>,
1637 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1639 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1641 pub mcp_servers: ScopedMcpServers,
1643 }
1649
1650#[derive(Debug, Clone, PartialEq, Eq)]
1651pub struct SystemPromptAttribution {
1652 pub capability_id: String,
1653 pub content: String,
1654}
1655
1656impl CollectedCapabilities {
1657 pub fn system_prompt_prefix(&self) -> Option<String> {
1660 if self.system_prompt_parts.is_empty() {
1661 None
1662 } else {
1663 Some(self.system_prompt_parts.join("\n\n"))
1664 }
1665 }
1666
1667 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1671 for (provider, config) in &self.message_filter_providers {
1673 provider.apply_filters(query, config);
1674 }
1675 }
1676
1677 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1680 for (provider, config) in &self.message_filter_providers {
1681 provider.post_load(messages, config);
1682 }
1683 }
1684
1685 pub fn has_message_filters(&self) -> bool {
1687 !self.message_filter_providers.is_empty()
1688 }
1689}
1690
1691pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1696 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1697 return base_system_prompt.to_string();
1698 };
1699
1700 if base_system_prompt.is_empty() {
1701 return additions.to_string();
1702 }
1703
1704 if base_system_prompt.contains("<system-prompt>") {
1705 format!("{base_system_prompt}\n\n{additions}")
1706 } else {
1707 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1708 }
1709}
1710
1711pub struct CollectedMessageFilters {
1718 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1720}
1721
1722pub struct CollectedModelViewProviders {
1724 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1726}
1727
1728impl CollectedMessageFilters {
1734 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1736 for (provider, config) in &self.message_filter_providers {
1737 provider.apply_filters(query, config);
1738 }
1739 }
1740
1741 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1743 for (provider, config) in &self.message_filter_providers {
1744 provider.post_load(messages, config);
1745 }
1746 }
1747}
1748
1749impl CollectedModelViewProviders {
1750 pub fn apply_model_view(
1752 &self,
1753 mut messages: Vec<Message>,
1754 context: &ModelViewContext<'_>,
1755 ) -> Vec<Message> {
1756 for (provider, config) in &self.model_view_providers {
1757 messages = provider.apply_model_view(messages, config, context);
1758 }
1759 messages
1760 }
1761}
1762
1763fn compaction_is_enabled(
1769 capability_configs: &[AgentCapabilityConfig],
1770 registry: &CapabilityRegistry,
1771) -> bool {
1772 capability_configs.iter().any(|cap_config| {
1773 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
1774 && registry
1775 .get(cap_config.capability_ref.as_str())
1776 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
1777 })
1778}
1779
1780fn message_filter_config_for(
1789 cap_id: &str,
1790 base: &serde_json::Value,
1791 compaction_on: bool,
1792) -> serde_json::Value {
1793 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
1794 return base.clone();
1795 }
1796 let mut config = base.clone();
1797 match config.as_object_mut() {
1798 Some(map) => {
1799 map.insert(
1800 "compaction_active".to_string(),
1801 serde_json::Value::Bool(true),
1802 );
1803 }
1804 None => {
1805 config = serde_json::json!({ "compaction_active": true });
1806 }
1807 }
1808 config
1809}
1810
1811pub fn collect_message_filters_only(
1817 capability_configs: &[AgentCapabilityConfig],
1818 registry: &CapabilityRegistry,
1819) -> CollectedMessageFilters {
1820 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1821 Vec::new();
1822 let compaction_on = compaction_is_enabled(capability_configs, registry);
1823
1824 for cap_config in capability_configs {
1825 let cap_id = cap_config.capability_ref.as_str();
1826 if let Some(capability) = registry.get(cap_id) {
1827 if capability.status() != CapabilityStatus::Available {
1828 continue;
1829 }
1830 let effective: &dyn Capability = capability
1833 .resolve_for_model(None)
1834 .unwrap_or_else(|| capability.as_ref());
1835 if let Some(provider) = effective.message_filter_provider() {
1836 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
1837 message_filter_providers.push((provider, config));
1838 }
1839 }
1840 }
1841
1842 message_filter_providers.sort_by_key(|(p, _)| p.priority());
1843
1844 CollectedMessageFilters {
1845 message_filter_providers,
1846 }
1847}
1848
1849pub fn collect_model_view_providers(
1856 capability_configs: &[AgentCapabilityConfig],
1857 registry: &CapabilityRegistry,
1858 model: Option<&str>,
1859) -> CollectedModelViewProviders {
1860 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1861
1862 for cap_config in capability_configs {
1863 let cap_id = cap_config.capability_ref.as_str();
1864 if let Some(capability) = registry.get(cap_id) {
1865 if capability.status() != CapabilityStatus::Available {
1866 continue;
1867 }
1868 let effective: &dyn Capability = capability
1869 .resolve_for_model(model)
1870 .unwrap_or_else(|| capability.as_ref());
1871 if let Some(provider) = effective.model_view_provider() {
1872 model_view_providers.push((provider, cap_config.config.clone()));
1873 }
1874 }
1875 }
1876
1877 model_view_providers.sort_by_key(|(p, _)| p.priority());
1878
1879 CollectedModelViewProviders {
1880 model_view_providers,
1881 }
1882}
1883
1884pub fn collect_dynamic_facts(
1890 capability_configs: &[AgentCapabilityConfig],
1891 registry: &CapabilityRegistry,
1892 model: Option<&str>,
1893 ctx: &FactsContext,
1894) -> Vec<Fact> {
1895 let mut dynamic = Vec::new();
1896 for cap_config in capability_configs {
1897 let cap_id = cap_config.capability_ref.as_str();
1898 if let Some(capability) = registry.get(cap_id) {
1899 if capability.status() != CapabilityStatus::Available {
1900 continue;
1901 }
1902 let effective: &dyn Capability = capability
1903 .resolve_for_model(model)
1904 .unwrap_or_else(|| capability.as_ref());
1905 for fact in effective.facts(&cap_config.config, ctx) {
1906 if fact.volatility == Volatility::Dynamic {
1907 dynamic.push(fact);
1908 }
1909 }
1910 }
1911 }
1912 dynamic
1913}
1914
1915pub fn collect_capability_mcp_servers(
1916 capability_configs: &[AgentCapabilityConfig],
1917 registry: &CapabilityRegistry,
1918) -> ScopedMcpServers {
1919 let mut servers = ScopedMcpServers::default();
1920
1921 for cap_config in capability_configs {
1922 let cap_id = cap_config.capability_ref.as_str();
1923 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
1926 if let Ok(definition) =
1927 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1928 {
1929 if definition.status != CapabilityStatus::Available {
1930 continue;
1931 }
1932 if let Some(contributed) = definition.mcp_servers {
1933 servers = merge_scoped_mcp_servers(&servers, &contributed);
1934 }
1935 }
1936 continue;
1937 }
1938 if let Some(capability) = registry.get(cap_id) {
1939 if capability.status() != CapabilityStatus::Available {
1940 continue;
1941 }
1942 servers = merge_scoped_mcp_servers(
1943 &servers,
1944 &capability.mcp_servers_with_config(&cap_config.config),
1945 );
1946 }
1947 }
1948
1949 servers
1950}
1951
1952pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1959
1960#[derive(Debug, Clone, PartialEq, Eq)]
1962pub enum DependencyError {
1963 CircularDependency {
1965 capability_id: String,
1967 chain: Vec<String>,
1969 },
1970 TooManyCapabilities {
1972 count: usize,
1974 max: usize,
1976 },
1977}
1978
1979impl std::fmt::Display for DependencyError {
1980 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1981 match self {
1982 DependencyError::CircularDependency {
1983 capability_id,
1984 chain,
1985 } => {
1986 write!(
1987 f,
1988 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1989 capability_id,
1990 chain.join(" -> "),
1991 capability_id
1992 )
1993 }
1994 DependencyError::TooManyCapabilities { count, max } => {
1995 write!(
1996 f,
1997 "Too many capabilities after resolution: {} (max: {})",
1998 count, max
1999 )
2000 }
2001 }
2002 }
2003}
2004
2005impl std::error::Error for DependencyError {}
2006
2007#[derive(Debug, Clone)]
2009pub struct ResolvedCapabilities {
2010 pub resolved_ids: Vec<String>,
2013 pub added_as_dependencies: Vec<String>,
2015 pub user_selected: Vec<String>,
2017}
2018
2019pub fn resolve_dependencies(
2039 selected_ids: &[String],
2040 registry: &CapabilityRegistry,
2041) -> Result<ResolvedCapabilities, DependencyError> {
2042 use std::collections::HashSet;
2043
2044 let user_selected: HashSet<String> = selected_ids
2046 .iter()
2047 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2048 .collect();
2049 let mut resolved: Vec<String> = Vec::new();
2050 let mut resolved_set: HashSet<String> = HashSet::new();
2051 let mut added_as_dependencies: Vec<String> = Vec::new();
2052
2053 for cap_id in selected_ids {
2055 resolve_single_capability(
2056 cap_id,
2057 registry,
2058 &mut resolved,
2059 &mut resolved_set,
2060 &mut added_as_dependencies,
2061 &user_selected,
2062 &mut Vec::new(), )?;
2064 }
2065
2066 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2068 return Err(DependencyError::TooManyCapabilities {
2069 count: resolved.len(),
2070 max: MAX_RESOLVED_CAPABILITIES,
2071 });
2072 }
2073
2074 Ok(ResolvedCapabilities {
2075 resolved_ids: resolved,
2076 added_as_dependencies,
2077 user_selected: selected_ids.to_vec(),
2078 })
2079}
2080
2081pub fn resolve_capability_configs(
2086 selected_configs: &[AgentCapabilityConfig],
2087 registry: &CapabilityRegistry,
2088) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2089 let mut selected_ids: Vec<String> = Vec::new();
2090 for config in selected_configs {
2091 if (is_declarative_capability(config.capability_id())
2094 || is_plugin_capability(config.capability_id()))
2095 && let Ok(definition) =
2096 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2097 {
2098 selected_ids.extend(definition.dependencies);
2099 }
2100 selected_ids.push(config.capability_id().to_string());
2101 }
2102 let resolved = resolve_dependencies(&selected_ids, registry)?;
2103
2104 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2107 .iter()
2108 .map(|config| {
2109 let id = config.capability_id();
2110 let id = registry.canonical_id(id).unwrap_or(id);
2111 (id.to_string(), config.config.clone())
2112 })
2113 .collect();
2114
2115 Ok(resolved
2116 .resolved_ids
2117 .into_iter()
2118 .map(|capability_id| {
2119 explicit_configs
2120 .get(&capability_id)
2121 .cloned()
2122 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2123 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2124 })
2125 .collect())
2126}
2127
2128fn resolve_single_capability(
2130 cap_id: &str,
2131 registry: &CapabilityRegistry,
2132 resolved: &mut Vec<String>,
2133 resolved_set: &mut std::collections::HashSet<String>,
2134 added_as_dependencies: &mut Vec<String>,
2135 user_selected: &std::collections::HashSet<String>,
2136 visiting: &mut Vec<String>,
2137) -> Result<(), DependencyError> {
2138 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2142
2143 if resolved_set.contains(cap_id) {
2145 return Ok(());
2146 }
2147
2148 if visiting.contains(&cap_id.to_string()) {
2150 return Err(DependencyError::CircularDependency {
2151 capability_id: cap_id.to_string(),
2152 chain: visiting.clone(),
2153 });
2154 }
2155
2156 let capability = match registry.get(cap_id) {
2158 Some(cap) => cap,
2159 None => {
2160 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2164 && !resolved_set.contains(cap_id)
2165 {
2166 resolved.push(cap_id.to_string());
2167 resolved_set.insert(cap_id.to_string());
2168 if !user_selected.contains(cap_id) {
2169 added_as_dependencies.push(cap_id.to_string());
2170 }
2171 }
2172 return Ok(());
2173 }
2174 };
2175
2176 visiting.push(cap_id.to_string());
2178
2179 for dep_id in capability.dependencies() {
2181 resolve_single_capability(
2182 dep_id,
2183 registry,
2184 resolved,
2185 resolved_set,
2186 added_as_dependencies,
2187 user_selected,
2188 visiting,
2189 )?;
2190 }
2191
2192 visiting.pop();
2194
2195 if !resolved_set.contains(cap_id) {
2197 resolved.push(cap_id.to_string());
2198 resolved_set.insert(cap_id.to_string());
2199
2200 if !user_selected.contains(cap_id) {
2202 added_as_dependencies.push(cap_id.to_string());
2203 }
2204 }
2205
2206 Ok(())
2207}
2208
2209pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2214 use std::collections::HashSet;
2215
2216 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2217 Ok(resolved) => resolved.resolved_ids,
2218 Err(_) => capability_ids.to_vec(),
2219 };
2220
2221 let mut seen = HashSet::new();
2222 let mut features = Vec::new();
2223 for cap_id in &resolved_ids {
2224 if let Some(cap) = registry.get(cap_id) {
2225 for feature in cap.features() {
2226 if seen.insert(feature) {
2227 features.push(feature.to_string());
2228 }
2229 }
2230 }
2231 }
2232 features
2233}
2234
2235pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2238 registry
2239 .get(cap_id)
2240 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2241 .unwrap_or_default()
2242}
2243
2244pub async fn collect_capabilities(
2260 capability_ids: &[String],
2261 registry: &CapabilityRegistry,
2262 ctx: &SystemPromptContext,
2263) -> CollectedCapabilities {
2264 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2267 Ok(resolved) => resolved.resolved_ids,
2268 Err(e) => {
2269 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2270 capability_ids.to_vec()
2271 }
2272 };
2273
2274 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2276 .iter()
2277 .map(|id| AgentCapabilityConfig {
2278 capability_ref: CapabilityId::new(id),
2279 config: serde_json::Value::Object(serde_json::Map::new()),
2280 })
2281 .collect();
2282
2283 collect_capabilities_with_configs(&configs, registry, ctx).await
2284}
2285
2286pub async fn collect_capabilities_with_configs(
2297 capability_configs: &[AgentCapabilityConfig],
2298 registry: &CapabilityRegistry,
2299 ctx: &SystemPromptContext,
2300) -> CollectedCapabilities {
2301 let mut system_prompt_parts: Vec<String> = Vec::new();
2302 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2303 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2304 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2305 let mut mounts: Vec<MountPoint> = Vec::new();
2306 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2307 Vec::new();
2308 let mut applied_ids: Vec<String> = Vec::new();
2309 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2310 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2311 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2312 let mut parallel_tool_calls: Option<bool> = None;
2313 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2314 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2315 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2318 let mut mcp_servers = ScopedMcpServers::default();
2319 let mut static_facts: Vec<Fact> = Vec::new();
2323 let mut has_dynamic_facts = false;
2324 let facts_ctx = FactsContext::new(ctx.session_id);
2325 let compaction_on = compaction_is_enabled(capability_configs, registry);
2326
2327 for cap_config in capability_configs {
2328 let cap_id = cap_config.capability_ref.as_str();
2329 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2334 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2335 cap_config.config.clone(),
2336 ) {
2337 Ok(definition) => {
2338 if definition.status != CapabilityStatus::Available {
2339 continue;
2340 }
2341
2342 if let Some(prompt) = definition.system_prompt.as_deref() {
2343 let contribution =
2344 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2345 system_prompt_attributions.push(SystemPromptAttribution {
2346 capability_id: cap_id.to_string(),
2347 content: contribution.clone(),
2348 });
2349 system_prompt_parts.push(contribution);
2350 }
2351
2352 mounts.extend(definition.mounts(cap_id));
2353 if let Some(ref servers) = definition.mcp_servers {
2354 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2355 }
2356 for skill in definition.skill_contributions() {
2357 mounts.push(skill.to_mount(cap_id));
2358 }
2359
2360 applied_ids.push(cap_id.to_string());
2361 }
2362 Err(error) => {
2363 tracing::warn!(
2364 capability_id = %cap_id,
2365 error = %error,
2366 "Skipping invalid declarative/plugin capability config"
2367 );
2368 }
2369 }
2370 continue;
2371 }
2372 if let Some(capability) = registry.get(cap_id) {
2373 if capability.status() != CapabilityStatus::Available {
2375 continue;
2376 }
2377
2378 let effective: &dyn Capability =
2390 match capability.resolve_for_model(ctx.model.as_deref()) {
2391 Some(inner) => inner,
2392 None => capability.as_ref(),
2393 };
2394 let effective_id = effective.id();
2395
2396 if let Some(contribution) = effective
2398 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2399 .await
2400 {
2401 system_prompt_attributions.push(SystemPromptAttribution {
2402 capability_id: cap_id.to_string(),
2403 content: contribution.clone(),
2404 });
2405 system_prompt_parts.push(contribution);
2406 }
2407
2408 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2413 match fact.volatility {
2414 Volatility::Static => static_facts.push(fact),
2415 Volatility::Dynamic => has_dynamic_facts = true,
2416 }
2417 }
2418
2419 tools.extend(effective.tools_with_config(&cap_config.config));
2421 tool_definition_hooks
2422 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2423 tool_call_hooks.extend(effective.tool_call_hooks());
2424 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2426 let cap_category = effective.category();
2431 for def in effective.tool_definitions() {
2432 let def = match (def.category(), cap_category) {
2433 (None, Some(cat)) => def.with_category(cat),
2434 _ => def,
2435 }
2436 .with_capability_attribution(cap_id, Some(capability.name()));
2437 tool_definitions.push(def);
2438 }
2439
2440 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2448 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2449 {
2450 let threshold = cap_config
2452 .config
2453 .get("threshold")
2454 .and_then(|v| v.as_u64())
2455 .map(|v| v as usize)
2456 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2457 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2458 enabled: true,
2459 threshold,
2460 });
2461 }
2462
2463 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2464 let strategy = cap_config
2465 .config
2466 .get("strategy")
2467 .and_then(|v| v.as_str())
2468 .map(|value| match value {
2469 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2470 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2471 })
2472 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2473 let gemini_cached_content = cap_config
2474 .config
2475 .get("gemini_cached_content")
2476 .and_then(|v| v.as_str())
2477 .map(str::to_string);
2478 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2479 enabled: true,
2480 strategy,
2481 gemini_cached_content,
2482 });
2483 }
2484
2485 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2486 parallel_tool_calls =
2487 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2488 }
2489
2490 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2491 let server_tools =
2492 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2493 if !server_tools.is_empty() {
2494 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2495 server_tools,
2496 ..Default::default()
2497 });
2498 }
2499 }
2500
2501 mounts.extend(effective.mounts());
2503
2504 mcp_servers = merge_scoped_mcp_servers(
2505 &mcp_servers,
2506 &effective.mcp_servers_with_config(&cap_config.config),
2507 );
2508
2509 for skill in effective.contribute_skills() {
2513 mounts.push(skill.to_mount(cap_id));
2514 }
2515
2516 if let Some(provider) = effective.message_filter_provider() {
2518 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2519 message_filter_providers.push((provider, config));
2520 }
2521
2522 applied_ids.push(cap_id.to_string());
2523 }
2524 }
2525
2526 if !applied_ids
2538 .iter()
2539 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2540 && tool_definitions
2541 .iter()
2542 .any(|def| def.hints().supports_background == Some(true))
2543 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2544 && bg_cap.status() == CapabilityStatus::Available
2545 {
2546 tools.extend(bg_cap.tools());
2547 let cap_category = bg_cap.category();
2548 for def in bg_cap.tool_definitions() {
2549 let def = match (def.category(), cap_category) {
2550 (None, Some(cat)) => def.with_category(cat),
2551 _ => def,
2552 }
2553 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2554 tool_definitions.push(def);
2555 }
2556 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2557 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2558 }
2559
2560 if let Some(block) = facts::render_facts_block(&static_facts) {
2565 system_prompt_attributions.push(SystemPromptAttribution {
2566 capability_id: "facts".to_string(),
2567 content: block.clone(),
2568 });
2569 system_prompt_parts.push(block);
2570 }
2571 if has_dynamic_facts {
2572 system_prompt_attributions.push(SystemPromptAttribution {
2573 capability_id: "facts".to_string(),
2574 content: FACTS_DYNAMIC_NOTE.to_string(),
2575 });
2576 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
2577 }
2578
2579 tool_call_hooks.extend(narration_hooks);
2583
2584 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2586
2587 CollectedCapabilities {
2588 system_prompt_parts,
2589 system_prompt_attributions,
2590 tools,
2591 tool_definitions,
2592 mounts,
2593 message_filter_providers,
2594 applied_ids,
2595 tool_search,
2596 prompt_cache,
2597 openrouter_routing,
2598 parallel_tool_calls,
2599 tool_definition_hooks,
2600 tool_call_hooks,
2601 mcp_servers,
2602 }
2603}
2604
2605pub struct AppliedCapabilities {
2611 pub runtime_agent: RuntimeAgent,
2613 pub tool_registry: ToolRegistry,
2615 pub applied_ids: Vec<String>,
2617}
2618
2619pub async fn apply_capabilities(
2656 base_runtime_agent: RuntimeAgent,
2657 capability_ids: &[String],
2658 registry: &CapabilityRegistry,
2659 ctx: &SystemPromptContext,
2660) -> AppliedCapabilities {
2661 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2662
2663 let final_system_prompt = compose_system_prompt(
2665 &base_runtime_agent.system_prompt,
2666 collected.system_prompt_prefix().as_deref(),
2667 );
2668
2669 let mut tool_registry = ToolRegistry::new();
2671 for tool in collected.tools {
2672 tool_registry.register_boxed(tool);
2673 }
2674
2675 let mut tools = collected.tool_definitions;
2677 for hook in &collected.tool_definition_hooks {
2678 tools = hook.transform(tools);
2679 }
2680
2681 let runtime_agent = RuntimeAgent {
2682 system_prompt: final_system_prompt,
2683 model: base_runtime_agent.model,
2684 tools,
2685 max_iterations: base_runtime_agent.max_iterations,
2686 temperature: base_runtime_agent.temperature,
2687 max_tokens: base_runtime_agent.max_tokens,
2688 tool_search: collected.tool_search,
2689 prompt_cache: collected.prompt_cache,
2690 openrouter_routing: collected.openrouter_routing,
2691 network_access: base_runtime_agent.network_access,
2692 parallel_tool_calls: base_runtime_agent
2695 .parallel_tool_calls
2696 .or(collected.parallel_tool_calls),
2697 };
2698
2699 AppliedCapabilities {
2700 runtime_agent,
2701 tool_registry,
2702 applied_ids: collected.applied_ids,
2703 }
2704}
2705
2706#[cfg(test)]
2711mod tests {
2712 use super::*;
2713 use crate::typed_id::SessionId;
2714 use std::collections::BTreeSet;
2715 use uuid::Uuid;
2716
2717 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2719
2720 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2721 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2722 }
2723
2724 fn test_ctx() -> SystemPromptContext {
2726 SystemPromptContext::without_file_store(SessionId::new())
2727 }
2728
2729 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2731 let mut ids = [
2732 "agent_instructions",
2733 "human_intent",
2734 "budgeting",
2735 "self_budget",
2736 "noop",
2737 "current_time",
2738 "research",
2739 "platform_management",
2740 "session_file_system",
2741 "session_storage",
2742 "session",
2743 "session_sql_database",
2744 "test_math",
2745 "test_weather",
2746 "stateless_todo_list",
2747 "web_fetch",
2748 "bashkit_shell",
2749 "background_execution",
2750 "session_schedule",
2751 "btw",
2752 "infinity_context",
2753 "compaction",
2754 "memory",
2755 "message_metadata",
2756 "openai_tool_search",
2757 "claude_tool_search",
2758 "tool_search",
2759 "auto_tool_search",
2760 "prompt_caching",
2761 "parallel_tool_calls",
2762 "session_tasks",
2763 "skills",
2764 "subagents",
2765 "system_commands",
2766 "sample_data",
2767 "data_knowledge",
2768 "knowledge_base",
2769 "knowledge_index",
2770 "tool_output_persistence",
2771 "tool_output_distillation",
2772 "fake_warehouse",
2773 "fake_aws",
2774 "fake_crm",
2775 "fake_financial",
2776 "loop_detection",
2777 "tool_call_repair",
2778 "error_disclosure",
2779 "prompt_canary_guardrail",
2780 "guardrails",
2781 "user_hooks",
2782 "model_scout",
2783 "openrouter_workspace",
2784 "openrouter_server_tools",
2785 ]
2786 .into_iter()
2787 .collect::<BTreeSet<_>>();
2788 if cfg!(feature = "ui-capabilities") {
2789 ids.insert("openui");
2790 ids.insert("a2ui");
2791 }
2792 ids
2793 }
2794
2795 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2797 let mut ids = expected_core_builtin_ids();
2798 ids.insert("agent_handoff");
2799 ids.insert("a2a_agent_delegation");
2800 ids
2801 }
2802
2803 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2804 registry.capabilities.keys().map(String::as_str).collect()
2805 }
2806
2807 #[test]
2817 fn test_capability_registry_with_builtins_dev() {
2818 let _lock = lock_env();
2820 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2821 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2822 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
2823 assert!(registry.has("agent_handoff"));
2824 assert!(registry.has("a2a_agent_delegation"));
2825 }
2826
2827 #[test]
2828 fn test_capability_registry_with_builtins_prod() {
2829 let _lock = lock_env();
2831 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2832 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2833 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
2834 assert!(!registry.has("docker_container"));
2836 assert!(!registry.has("agent_handoff"));
2837 assert!(!registry.has("a2a_agent_delegation"));
2838 }
2839
2840 #[test]
2841 fn test_agent_delegation_enabled_by_env_in_prod() {
2842 let _lock = lock_env();
2844 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2845 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2846 assert!(registry.has("agent_handoff"));
2847 assert!(registry.has("a2a_agent_delegation"));
2848 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2849 }
2850
2851 #[test]
2852 fn test_agent_delegation_disabled_by_env_in_dev() {
2853 let _lock = lock_env();
2855 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2856 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2857 assert!(!registry.has("agent_handoff"));
2858 assert!(!registry.has("a2a_agent_delegation"));
2859 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2860 }
2861
2862 #[test]
2863 fn test_capability_registry_get() {
2864 let registry = CapabilityRegistry::with_builtins();
2865
2866 let noop = registry.get("noop").unwrap();
2867 assert_eq!(noop.id(), "noop");
2868 assert_eq!(noop.name(), "No-Op");
2869 assert_eq!(noop.status(), CapabilityStatus::Available);
2870 }
2871
2872 #[test]
2880 fn builtin_capabilities_satisfy_registry_invariants() {
2881 let registry = CapabilityRegistry::with_builtins();
2882
2883 for cap in registry.list() {
2884 let id = cap.id();
2885 assert!(!id.is_empty(), "capability has an empty id");
2886 assert!(
2887 !cap.name().trim().is_empty(),
2888 "capability `{id}` has an empty name"
2889 );
2890
2891 assert!(
2894 registry.get(id).is_some(),
2895 "capability `{id}` does not resolve by its own id"
2896 );
2897
2898 for dep in cap.dependencies() {
2902 assert!(
2903 registry.get(dep).is_some(),
2904 "capability `{id}` depends on `{dep}`, which is not registered"
2905 );
2906 }
2907
2908 let mut seen = std::collections::HashSet::new();
2911 for tool in cap.tools() {
2912 let name = tool.name().to_string();
2913 assert!(
2914 !name.is_empty(),
2915 "capability `{id}` exposes a tool with an empty name"
2916 );
2917 assert!(
2918 seen.insert(name.clone()),
2919 "capability `{id}` exposes duplicate tool name `{name}`"
2920 );
2921 }
2922
2923 let mut def_seen = std::collections::HashSet::new();
2926 for def in cap.tool_definitions() {
2927 let name = def.name().to_string();
2928 assert!(
2929 !name.is_empty(),
2930 "capability `{id}` advertises a tool definition with an empty name"
2931 );
2932 assert!(
2933 def_seen.insert(name.clone()),
2934 "capability `{id}` advertises duplicate tool definition name `{name}`"
2935 );
2936 }
2937 }
2938 }
2939
2940 #[test]
2941 fn test_capability_registry_blueprint_with_capability() {
2942 struct BlueprintProviderCapability;
2943
2944 impl Capability for BlueprintProviderCapability {
2945 fn id(&self) -> &str {
2946 "blueprint_provider"
2947 }
2948 fn name(&self) -> &str {
2949 "Blueprint Provider"
2950 }
2951 fn description(&self) -> &str {
2952 "Capability that provides a blueprint for tests"
2953 }
2954 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2955 vec![AgentBlueprint {
2956 id: "test_blueprint",
2957 name: "Test Blueprint",
2958 description: "Blueprint for capability registry tests",
2959 model: BlueprintModel::Inherit,
2960 system_prompt: "Test prompt",
2961 tools: vec![],
2962 max_turns: None,
2963 config_schema: None,
2964 }]
2965 }
2966 }
2967
2968 let mut registry = CapabilityRegistry::new();
2969 registry.register(BlueprintProviderCapability);
2970
2971 let (capability_id, blueprint) = registry
2972 .blueprint_with_capability("test_blueprint")
2973 .expect("blueprint should resolve with capability id");
2974 assert_eq!(capability_id, "blueprint_provider");
2975 assert_eq!(blueprint.id, "test_blueprint");
2976 }
2977
2978 #[test]
2979 fn test_capability_registry_builder() {
2980 let registry = CapabilityRegistry::builder()
2981 .capability(NoopCapability)
2982 .capability(CurrentTimeCapability)
2983 .build();
2984
2985 assert!(registry.has("noop"));
2986 assert!(registry.has("current_time"));
2987 assert_eq!(registry.len(), 2);
2988 }
2989
2990 #[test]
2991 fn test_capability_status() {
2992 let registry = CapabilityRegistry::with_builtins();
2993
2994 let current_time = registry.get("current_time").unwrap();
2995 assert_eq!(current_time.status(), CapabilityStatus::Available);
2996
2997 let research = registry.get("research").unwrap();
2998 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2999 }
3000
3001 #[test]
3002 fn test_capability_icons_and_categories() {
3003 let registry = CapabilityRegistry::with_builtins();
3004
3005 let noop = registry.get("noop").unwrap();
3006 assert_eq!(noop.icon(), Some("circle-off"));
3007 assert_eq!(noop.category(), Some("Testing"));
3008
3009 let current_time = registry.get("current_time").unwrap();
3010 assert_eq!(current_time.icon(), Some("clock"));
3011 assert_eq!(current_time.category(), Some("Core"));
3012 }
3013
3014 #[test]
3015 fn test_system_prompt_preview_default_delegates_to_addition() {
3016 let registry = CapabilityRegistry::with_builtins();
3017
3018 let test_math = registry.get("test_math").unwrap();
3020 assert_eq!(
3021 test_math.system_prompt_preview().as_deref(),
3022 test_math.system_prompt_addition()
3023 );
3024
3025 let current_time = registry.get("current_time").unwrap();
3027 assert!(current_time.system_prompt_preview().is_none());
3028 assert!(current_time.system_prompt_addition().is_none());
3029 }
3030
3031 #[test]
3032 fn test_system_prompt_preview_dynamic_capability() {
3033 let registry = CapabilityRegistry::with_builtins();
3034 let cap = registry.get("agent_instructions").unwrap();
3035
3036 assert!(cap.system_prompt_addition().is_none());
3038 assert!(cap.system_prompt_preview().is_some());
3039 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3040 }
3041
3042 #[tokio::test]
3047 async fn test_apply_capabilities_empty() {
3048 let registry = CapabilityRegistry::with_builtins();
3049 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3050
3051 let applied =
3052 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3053
3054 assert_eq!(
3055 applied.runtime_agent.system_prompt,
3056 base_runtime_agent.system_prompt
3057 );
3058 assert!(applied.tool_registry.is_empty());
3059 assert!(applied.applied_ids.is_empty());
3060 }
3061
3062 #[tokio::test]
3063 async fn test_apply_capabilities_noop() {
3064 let registry = CapabilityRegistry::with_builtins();
3065 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3066
3067 let applied = apply_capabilities(
3068 base_runtime_agent.clone(),
3069 &["noop".to_string()],
3070 ®istry,
3071 &test_ctx(),
3072 )
3073 .await;
3074
3075 assert_eq!(
3077 applied.runtime_agent.system_prompt,
3078 base_runtime_agent.system_prompt
3079 );
3080 assert!(applied.tool_registry.is_empty());
3081 assert_eq!(applied.applied_ids, vec!["noop"]);
3082 }
3083
3084 #[tokio::test]
3085 async fn test_apply_capabilities_current_time() {
3086 let registry = CapabilityRegistry::with_builtins();
3087 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3088
3089 let applied = apply_capabilities(
3090 base_runtime_agent.clone(),
3091 &["current_time".to_string()],
3092 ®istry,
3093 &test_ctx(),
3094 )
3095 .await;
3096
3097 assert!(
3101 applied
3102 .runtime_agent
3103 .system_prompt
3104 .contains(FACTS_DYNAMIC_NOTE),
3105 "current_time should contribute the dynamic-facts note"
3106 );
3107 assert!(
3108 applied
3109 .runtime_agent
3110 .system_prompt
3111 .contains(&base_runtime_agent.system_prompt),
3112 "base prompt is preserved"
3113 );
3114 assert!(applied.tool_registry.has("get_current_time"));
3115 assert_eq!(applied.tool_registry.len(), 1);
3116 assert_eq!(applied.applied_ids, vec!["current_time"]);
3117 }
3118
3119 #[tokio::test]
3120 async fn test_apply_capabilities_skips_coming_soon() {
3121 let registry = CapabilityRegistry::with_builtins();
3122 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3123
3124 let applied = apply_capabilities(
3126 base_runtime_agent.clone(),
3127 &["research".to_string()],
3128 ®istry,
3129 &test_ctx(),
3130 )
3131 .await;
3132
3133 assert_eq!(
3135 applied.runtime_agent.system_prompt,
3136 base_runtime_agent.system_prompt
3137 );
3138 assert!(applied.applied_ids.is_empty()); }
3140
3141 #[tokio::test]
3142 async fn test_apply_capabilities_multiple() {
3143 let registry = CapabilityRegistry::with_builtins();
3144 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3145
3146 let applied = apply_capabilities(
3147 base_runtime_agent.clone(),
3148 &["noop".to_string(), "current_time".to_string()],
3149 ®istry,
3150 &test_ctx(),
3151 )
3152 .await;
3153
3154 assert!(applied.tool_registry.has("get_current_time"));
3155 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3156 }
3157
3158 #[tokio::test]
3159 async fn test_apply_capabilities_preserves_order() {
3160 let registry = CapabilityRegistry::with_builtins();
3161 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3162
3163 let applied = apply_capabilities(
3165 base_runtime_agent,
3166 &["current_time".to_string(), "noop".to_string()],
3167 ®istry,
3168 &test_ctx(),
3169 )
3170 .await;
3171
3172 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3173 }
3174
3175 #[tokio::test]
3176 async fn test_apply_capabilities_test_math() {
3177 let registry = CapabilityRegistry::with_builtins();
3178 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3179
3180 let applied = apply_capabilities(
3181 base_runtime_agent.clone(),
3182 &["test_math".to_string()],
3183 ®istry,
3184 &test_ctx(),
3185 )
3186 .await;
3187
3188 assert!(
3190 !applied
3191 .runtime_agent
3192 .system_prompt
3193 .contains("<capability id=\"test_math\">")
3194 );
3195 assert!(
3197 applied
3198 .runtime_agent
3199 .system_prompt
3200 .contains("You are a helpful assistant.")
3201 );
3202 assert!(applied.tool_registry.has("add"));
3203 assert!(applied.tool_registry.has("subtract"));
3204 assert!(applied.tool_registry.has("multiply"));
3205 assert!(applied.tool_registry.has("divide"));
3206 assert_eq!(applied.tool_registry.len(), 4);
3207 }
3208
3209 #[tokio::test]
3210 async fn test_apply_capabilities_test_weather() {
3211 let registry = CapabilityRegistry::with_builtins();
3212 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3213
3214 let applied = apply_capabilities(
3215 base_runtime_agent.clone(),
3216 &["test_weather".to_string()],
3217 ®istry,
3218 &test_ctx(),
3219 )
3220 .await;
3221
3222 assert!(
3224 !applied
3225 .runtime_agent
3226 .system_prompt
3227 .contains("<capability id=\"test_weather\">")
3228 );
3229 assert!(applied.tool_registry.has("get_weather"));
3230 assert!(applied.tool_registry.has("get_forecast"));
3231 assert_eq!(applied.tool_registry.len(), 2);
3232 }
3233
3234 #[tokio::test]
3235 async fn test_apply_capabilities_test_math_and_test_weather() {
3236 let registry = CapabilityRegistry::with_builtins();
3237 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3238
3239 let applied = apply_capabilities(
3240 base_runtime_agent.clone(),
3241 &["test_math".to_string(), "test_weather".to_string()],
3242 ®istry,
3243 &test_ctx(),
3244 )
3245 .await;
3246
3247 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3250 assert!(applied.tool_registry.has("get_weather"));
3251 }
3252
3253 #[tokio::test]
3254 async fn test_apply_capabilities_stateless_todo_list() {
3255 let registry = CapabilityRegistry::with_builtins();
3256 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3257
3258 let applied = apply_capabilities(
3259 base_runtime_agent.clone(),
3260 &["stateless_todo_list".to_string()],
3261 ®istry,
3262 &test_ctx(),
3263 )
3264 .await;
3265
3266 assert!(
3268 applied
3269 .runtime_agent
3270 .system_prompt
3271 .contains("Task Management")
3272 );
3273 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3274 assert!(applied.tool_registry.has("write_todos"));
3275 assert_eq!(applied.tool_registry.len(), 1);
3276 }
3277
3278 #[tokio::test]
3279 async fn test_apply_capabilities_web_fetch() {
3280 let registry = CapabilityRegistry::with_builtins();
3281 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3282
3283 let applied = apply_capabilities(
3284 base_runtime_agent.clone(),
3285 &["web_fetch".to_string()],
3286 ®istry,
3287 &test_ctx(),
3288 )
3289 .await;
3290
3291 assert!(
3293 applied
3294 .runtime_agent
3295 .system_prompt
3296 .contains(&base_runtime_agent.system_prompt)
3297 );
3298 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3299 assert!(applied.tool_registry.has("web_fetch"));
3300 assert_eq!(applied.tool_registry.len(), 1);
3301 }
3302
3303 #[tokio::test]
3308 async fn test_xml_tags_wrap_capability_prompts() {
3309 let registry = CapabilityRegistry::with_builtins();
3310 let collected =
3311 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3312 .await;
3313
3314 assert_eq!(collected.system_prompt_parts.len(), 1);
3315 let part = &collected.system_prompt_parts[0];
3316 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3317 assert!(part.ends_with("</capability>"));
3318 assert!(part.contains("Task Management"));
3319 }
3320
3321 #[tokio::test]
3322 async fn test_xml_tags_multiple_capabilities() {
3323 let registry = CapabilityRegistry::with_builtins();
3324 let collected = collect_capabilities(
3325 &[
3326 "stateless_todo_list".to_string(),
3327 "session_schedule".to_string(),
3328 ],
3329 ®istry,
3330 &test_ctx(),
3331 )
3332 .await;
3333
3334 assert_eq!(collected.system_prompt_parts.len(), 2);
3335 assert!(
3336 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3337 );
3338 assert!(
3339 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3340 );
3341
3342 let prefix = collected.system_prompt_prefix().unwrap();
3343 assert!(prefix.contains("</capability>\n\n<capability"));
3345 }
3346
3347 #[tokio::test]
3348 async fn test_xml_tags_system_prompt_wrapping() {
3349 let registry = CapabilityRegistry::with_builtins();
3350 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3351
3352 let applied = apply_capabilities(
3353 base,
3354 &["stateless_todo_list".to_string()],
3355 ®istry,
3356 &test_ctx(),
3357 )
3358 .await;
3359
3360 let prompt = &applied.runtime_agent.system_prompt;
3361 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3362 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3364 assert!(prompt.contains("</capability>"));
3365 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3367 }
3368
3369 #[tokio::test]
3370 async fn test_no_xml_wrapping_without_capabilities() {
3371 let registry = CapabilityRegistry::with_builtins();
3372 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3373
3374 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3375
3376 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3378 assert!(
3379 !applied
3380 .runtime_agent
3381 .system_prompt
3382 .contains("<system-prompt>")
3383 );
3384 }
3385
3386 #[tokio::test]
3387 async fn test_no_xml_wrapping_for_noop_capability() {
3388 let registry = CapabilityRegistry::with_builtins();
3389 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3390
3391 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3393
3394 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3395 assert!(
3396 !applied
3397 .runtime_agent
3398 .system_prompt
3399 .contains("<system-prompt>")
3400 );
3401 }
3402
3403 #[tokio::test]
3408 async fn test_collect_capabilities_includes_mounts() {
3409 let registry = CapabilityRegistry::with_builtins();
3410
3411 let collected =
3412 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3413
3414 assert!(!collected.mounts.is_empty());
3415 assert_eq!(collected.mounts.len(), 1);
3416 assert_eq!(collected.mounts[0].path, "/samples");
3417 assert!(collected.mounts[0].is_readonly());
3418 }
3419
3420 #[tokio::test]
3421 async fn test_collect_capabilities_empty_mounts_by_default() {
3422 let registry = CapabilityRegistry::with_builtins();
3423
3424 let collected =
3426 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3427
3428 assert!(collected.mounts.is_empty());
3429 }
3430
3431 #[tokio::test]
3432 async fn test_dynamic_facts_add_note_without_static_block() {
3433 let registry = CapabilityRegistry::with_builtins();
3437 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3438 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3439 let prompt = collected.system_prompt_parts.join("\n");
3440 assert!(
3441 prompt.contains(FACTS_DYNAMIC_NOTE),
3442 "dynamic-facts note should be in the cached prompt"
3443 );
3444 assert!(
3445 !prompt.contains("<facts>\n"),
3446 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
3447 );
3448 }
3449
3450 #[tokio::test]
3451 async fn test_static_facts_fold_into_prompt() {
3452 struct StaticFactCap;
3453 impl Capability for StaticFactCap {
3454 fn id(&self) -> &str {
3455 "test_static_fact"
3456 }
3457 fn name(&self) -> &str {
3458 "Static Fact"
3459 }
3460 fn description(&self) -> &str {
3461 "test"
3462 }
3463 fn status(&self) -> CapabilityStatus {
3464 CapabilityStatus::Available
3465 }
3466 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
3467 vec![Fact::stat("workspace_root", "/workspace")]
3468 }
3469 }
3470 let mut registry = CapabilityRegistry::new();
3471 registry.register(StaticFactCap);
3472 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
3473 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3474 let prompt = collected.system_prompt_parts.join("\n");
3475 assert!(
3476 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
3477 "static fact should fold into the cached prompt; got: {prompt}"
3478 );
3479 assert!(
3480 !prompt.contains(FACTS_DYNAMIC_NOTE),
3481 "no dynamic note when only static facts exist"
3482 );
3483 }
3484
3485 #[test]
3486 fn test_collect_dynamic_facts_returns_current_time() {
3487 let registry = CapabilityRegistry::with_builtins();
3488 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3489 let facts = collect_dynamic_facts(
3490 &configs,
3491 ®istry,
3492 None,
3493 &FactsContext::new(SessionId::new()),
3494 );
3495 assert_eq!(facts.len(), 1);
3496 assert_eq!(facts[0].key, "current_time");
3497 assert_eq!(facts[0].volatility, Volatility::Dynamic);
3498 }
3499
3500 #[tokio::test]
3501 async fn test_collect_capabilities_combines_mounts() {
3502 let registry = CapabilityRegistry::with_builtins();
3503
3504 let collected = collect_capabilities(
3507 &["sample_data".to_string(), "current_time".to_string()],
3508 ®istry,
3509 &test_ctx(),
3510 )
3511 .await;
3512
3513 assert_eq!(collected.mounts.len(), 1);
3514 assert!(
3516 collected
3517 .applied_ids
3518 .iter()
3519 .any(|id| id == "session_file_system")
3520 );
3521 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3522 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3523 }
3524
3525 #[test]
3526 fn test_sample_data_capability() {
3527 let registry = CapabilityRegistry::with_builtins();
3528 let cap = registry.get("sample_data").unwrap();
3529
3530 assert_eq!(cap.id(), "sample_data");
3531 assert_eq!(cap.name(), "Sample Data");
3532 assert_eq!(cap.status(), CapabilityStatus::Available);
3533
3534 assert!(cap.system_prompt_addition().is_some());
3536 assert!(cap.tools().is_empty());
3537
3538 assert!(!cap.mounts().is_empty());
3540 }
3541
3542 #[test]
3547 fn test_resolve_dependencies_empty() {
3548 let registry = CapabilityRegistry::with_builtins();
3549
3550 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3551
3552 assert!(resolved.resolved_ids.is_empty());
3553 assert!(resolved.added_as_dependencies.is_empty());
3554 assert!(resolved.user_selected.is_empty());
3555 }
3556
3557 #[test]
3558 fn test_resolve_dependencies_no_deps() {
3559 let registry = CapabilityRegistry::with_builtins();
3560
3561 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3563
3564 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3565 assert!(resolved.added_as_dependencies.is_empty());
3566 }
3567
3568 #[test]
3569 fn test_resolve_dependencies_with_deps() {
3570 let registry = CapabilityRegistry::with_builtins();
3571
3572 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3574
3575 assert_eq!(resolved.resolved_ids.len(), 2);
3577 let fs_pos = resolved
3578 .resolved_ids
3579 .iter()
3580 .position(|id| id == "session_file_system")
3581 .unwrap();
3582 let sd_pos = resolved
3583 .resolved_ids
3584 .iter()
3585 .position(|id| id == "sample_data")
3586 .unwrap();
3587 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3588
3589 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3591 }
3592
3593 #[test]
3594 fn test_resolve_dependencies_already_selected() {
3595 let registry = CapabilityRegistry::with_builtins();
3596
3597 let resolved = resolve_dependencies(
3599 &["session_file_system".to_string(), "sample_data".to_string()],
3600 ®istry,
3601 )
3602 .unwrap();
3603
3604 assert_eq!(resolved.resolved_ids.len(), 2);
3605 assert!(resolved.added_as_dependencies.is_empty());
3607 }
3608
3609 #[test]
3610 fn test_resolve_dependencies_preserves_order() {
3611 let registry = CapabilityRegistry::with_builtins();
3612
3613 let resolved =
3615 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
3616 .unwrap();
3617
3618 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
3619 }
3620
3621 #[test]
3622 fn test_resolve_dependencies_unknown_capability() {
3623 let registry = CapabilityRegistry::with_builtins();
3624
3625 let resolved =
3627 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
3628
3629 assert!(resolved.resolved_ids.is_empty());
3630 }
3631
3632 #[test]
3633 fn test_get_dependencies() {
3634 let registry = CapabilityRegistry::with_builtins();
3635
3636 let deps = get_dependencies("sample_data", ®istry);
3638 assert_eq!(deps, vec!["session_file_system"]);
3639
3640 let deps = get_dependencies("current_time", ®istry);
3642 assert!(deps.is_empty());
3643
3644 let deps = get_dependencies("unknown", ®istry);
3646 assert!(deps.is_empty());
3647 }
3648
3649 #[test]
3650 fn test_sample_data_has_dependency() {
3651 let registry = CapabilityRegistry::with_builtins();
3652 let cap = registry.get("sample_data").unwrap();
3653
3654 let deps = cap.dependencies();
3655 assert_eq!(deps.len(), 1);
3656 assert_eq!(deps[0], "session_file_system");
3657 }
3658
3659 #[test]
3660 fn test_noop_has_no_dependencies() {
3661 let registry = CapabilityRegistry::with_builtins();
3662 let cap = registry.get("noop").unwrap();
3663
3664 assert!(cap.dependencies().is_empty());
3665 }
3666
3667 #[test]
3671 fn test_circular_dependency_error() {
3672 struct CapA;
3674 struct CapB;
3675
3676 impl Capability for CapA {
3677 fn id(&self) -> &str {
3678 "test_cap_a"
3679 }
3680 fn name(&self) -> &str {
3681 "Test A"
3682 }
3683 fn description(&self) -> &str {
3684 "Test capability A"
3685 }
3686 fn dependencies(&self) -> Vec<&'static str> {
3687 vec!["test_cap_b"]
3688 }
3689 }
3690
3691 impl Capability for CapB {
3692 fn id(&self) -> &str {
3693 "test_cap_b"
3694 }
3695 fn name(&self) -> &str {
3696 "Test B"
3697 }
3698 fn description(&self) -> &str {
3699 "Test capability B"
3700 }
3701 fn dependencies(&self) -> Vec<&'static str> {
3702 vec!["test_cap_a"]
3703 }
3704 }
3705
3706 let mut registry = CapabilityRegistry::new();
3707 registry.register(CapA);
3708 registry.register(CapB);
3709
3710 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
3711
3712 assert!(result.is_err());
3713 match result.unwrap_err() {
3714 DependencyError::CircularDependency { capability_id, .. } => {
3715 assert_eq!(capability_id, "test_cap_a");
3716 }
3717 _ => panic!("Expected CircularDependency error"),
3718 }
3719 }
3720
3721 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
3726
3727 struct FilterTestCapability {
3729 priority: i32,
3730 }
3731
3732 impl Capability for FilterTestCapability {
3733 fn id(&self) -> &str {
3734 "filter_test"
3735 }
3736 fn name(&self) -> &str {
3737 "Filter Test"
3738 }
3739 fn description(&self) -> &str {
3740 "Test capability with message filter"
3741 }
3742 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3743 Some(Arc::new(FilterTestProvider {
3744 priority: self.priority,
3745 }))
3746 }
3747 }
3748
3749 struct FilterTestProvider {
3750 priority: i32,
3751 }
3752
3753 impl MessageFilterProvider for FilterTestProvider {
3754 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
3755 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
3757 query
3758 .filters
3759 .push(MessageFilter::Search(search.to_string()));
3760 }
3761 }
3762
3763 fn priority(&self) -> i32 {
3764 self.priority
3765 }
3766 }
3767
3768 #[tokio::test]
3769 async fn test_collect_capabilities_with_configs_no_filter_providers() {
3770 let registry = CapabilityRegistry::with_builtins();
3771 let configs = vec![AgentCapabilityConfig {
3772 capability_ref: CapabilityId::new("current_time"),
3773 config: serde_json::json!({}),
3774 }];
3775
3776 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3777
3778 assert!(collected.message_filter_providers.is_empty());
3779 assert!(!collected.has_message_filters());
3780 }
3781
3782 #[tokio::test]
3783 async fn test_collect_capabilities_with_configs_with_filter_provider() {
3784 let mut registry = CapabilityRegistry::new();
3785 registry.register(FilterTestCapability { priority: 0 });
3786
3787 let configs = vec![AgentCapabilityConfig {
3788 capability_ref: CapabilityId::new("filter_test"),
3789 config: serde_json::json!({ "search": "hello" }),
3790 }];
3791
3792 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3793
3794 assert_eq!(collected.message_filter_providers.len(), 1);
3795 assert!(collected.has_message_filters());
3796 }
3797
3798 #[tokio::test]
3799 async fn test_collect_capabilities_with_configs_filter_priority_order() {
3800 struct HighPriorityCapability;
3802 struct LowPriorityCapability;
3803
3804 impl Capability for HighPriorityCapability {
3805 fn id(&self) -> &str {
3806 "high_priority"
3807 }
3808 fn name(&self) -> &str {
3809 "High Priority"
3810 }
3811 fn description(&self) -> &str {
3812 "Test"
3813 }
3814 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3815 Some(Arc::new(FilterTestProvider { priority: 10 }))
3816 }
3817 }
3818
3819 impl Capability for LowPriorityCapability {
3820 fn id(&self) -> &str {
3821 "low_priority"
3822 }
3823 fn name(&self) -> &str {
3824 "Low Priority"
3825 }
3826 fn description(&self) -> &str {
3827 "Test"
3828 }
3829 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3830 Some(Arc::new(FilterTestProvider { priority: -5 }))
3831 }
3832 }
3833
3834 let mut registry = CapabilityRegistry::new();
3835 registry.register(HighPriorityCapability);
3836 registry.register(LowPriorityCapability);
3837
3838 let configs = vec![
3840 AgentCapabilityConfig {
3841 capability_ref: CapabilityId::new("high_priority"),
3842 config: serde_json::json!({}),
3843 },
3844 AgentCapabilityConfig {
3845 capability_ref: CapabilityId::new("low_priority"),
3846 config: serde_json::json!({}),
3847 },
3848 ];
3849
3850 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3851
3852 assert_eq!(collected.message_filter_providers.len(), 2);
3854 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
3855 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
3856 }
3857
3858 #[tokio::test]
3859 async fn test_collected_capabilities_apply_message_filters() {
3860 let mut registry = CapabilityRegistry::new();
3861 registry.register(FilterTestCapability { priority: 0 });
3862
3863 let configs = vec![AgentCapabilityConfig {
3864 capability_ref: CapabilityId::new("filter_test"),
3865 config: serde_json::json!({ "search": "test_query" }),
3866 }];
3867
3868 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3869
3870 let session_id: SessionId = Uuid::now_v7().into();
3872 let mut query = MessageQuery::new(session_id);
3873
3874 collected.apply_message_filters(&mut query);
3875
3876 assert_eq!(query.filters.len(), 1);
3878 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3879 }
3880
3881 #[tokio::test]
3882 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3883 struct SearchCapability {
3884 id: &'static str,
3885 search_term: &'static str,
3886 priority: i32,
3887 }
3888
3889 struct SearchProvider {
3890 search_term: &'static str,
3891 priority: i32,
3892 }
3893
3894 impl MessageFilterProvider for SearchProvider {
3895 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3896 query
3897 .filters
3898 .push(MessageFilter::Search(self.search_term.to_string()));
3899 }
3900
3901 fn priority(&self) -> i32 {
3902 self.priority
3903 }
3904 }
3905
3906 impl Capability for SearchCapability {
3907 fn id(&self) -> &str {
3908 self.id
3909 }
3910 fn name(&self) -> &str {
3911 "Search"
3912 }
3913 fn description(&self) -> &str {
3914 "Test"
3915 }
3916 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3917 Some(Arc::new(SearchProvider {
3918 search_term: self.search_term,
3919 priority: self.priority,
3920 }))
3921 }
3922 }
3923
3924 let mut registry = CapabilityRegistry::new();
3925 registry.register(SearchCapability {
3926 id: "cap_a",
3927 search_term: "alpha",
3928 priority: 5,
3929 });
3930 registry.register(SearchCapability {
3931 id: "cap_b",
3932 search_term: "beta",
3933 priority: 1,
3934 });
3935 registry.register(SearchCapability {
3936 id: "cap_c",
3937 search_term: "gamma",
3938 priority: 10,
3939 });
3940
3941 let configs = vec![
3942 AgentCapabilityConfig {
3943 capability_ref: CapabilityId::new("cap_a"),
3944 config: serde_json::json!({}),
3945 },
3946 AgentCapabilityConfig {
3947 capability_ref: CapabilityId::new("cap_b"),
3948 config: serde_json::json!({}),
3949 },
3950 AgentCapabilityConfig {
3951 capability_ref: CapabilityId::new("cap_c"),
3952 config: serde_json::json!({}),
3953 },
3954 ];
3955
3956 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3957
3958 let session_id: SessionId = Uuid::now_v7().into();
3959 let mut query = MessageQuery::new(session_id);
3960
3961 collected.apply_message_filters(&mut query);
3962
3963 assert_eq!(query.filters.len(), 3);
3965 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3966 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3967 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3968 }
3969
3970 #[test]
3971 fn test_capability_without_message_filter_returns_none() {
3972 let registry = CapabilityRegistry::with_builtins();
3973
3974 let noop = registry.get("noop").unwrap();
3975 assert!(noop.message_filter_provider().is_none());
3976
3977 let current_time = registry.get("current_time").unwrap();
3978 assert!(current_time.message_filter_provider().is_none());
3979 }
3980
3981 #[tokio::test]
3982 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3983 let mut registry = CapabilityRegistry::new();
3984 registry.register(FilterTestCapability { priority: 0 });
3985
3986 let test_config = serde_json::json!({
3987 "search": "custom_search",
3988 "extra_field": 42
3989 });
3990
3991 let configs = vec![AgentCapabilityConfig {
3992 capability_ref: CapabilityId::new("filter_test"),
3993 config: test_config.clone(),
3994 }];
3995
3996 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3997
3998 assert_eq!(collected.message_filter_providers.len(), 1);
4000 let (_, stored_config) = &collected.message_filter_providers[0];
4001 assert_eq!(*stored_config, test_config);
4002 }
4003
4004 #[test]
4009 fn test_collect_message_filters_only_collects_filters() {
4010 let mut registry = CapabilityRegistry::new();
4011 registry.register(FilterTestCapability { priority: 0 });
4012
4013 let configs = vec![AgentCapabilityConfig {
4014 capability_ref: CapabilityId::new("filter_test"),
4015 config: serde_json::json!({ "search": "test_query" }),
4016 }];
4017
4018 let collected = collect_message_filters_only(&configs, ®istry);
4019
4020 let session_id: SessionId = Uuid::now_v7().into();
4021 let mut query = MessageQuery::new(session_id);
4022 collected.apply_message_filters(&mut query);
4023
4024 assert_eq!(query.filters.len(), 1);
4025 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4026 }
4027
4028 #[test]
4029 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4030 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4031
4032 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4034 assert_eq!(with["compaction_active"], serde_json::json!(true));
4035 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4036
4037 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4038 assert!(without.get("compaction_active").is_none());
4039
4040 let other = message_filter_config_for("other", &base, true);
4042 assert!(other.get("compaction_active").is_none());
4043
4044 let null_base = message_filter_config_for(
4046 INFINITY_CONTEXT_CAPABILITY_ID,
4047 &serde_json::Value::Null,
4048 true,
4049 );
4050 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4051 }
4052
4053 #[test]
4054 fn test_infinity_context_defers_to_compaction_end_to_end() {
4055 use crate::message::Message;
4056
4057 let mut registry = CapabilityRegistry::new();
4058 registry.register(InfinityContextCapability);
4059 registry.register(CompactionCapability);
4060
4061 let tight = serde_json::json!({
4062 "context_budget_tokens": 1,
4063 "min_recent_messages": 1
4064 });
4065
4066 let solo = vec![AgentCapabilityConfig {
4068 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4069 config: tight.clone(),
4070 }];
4071 let mut messages = vec![
4072 Message::user("task"),
4073 Message::assistant("old ".repeat(400)),
4074 Message::user("recent"),
4075 ];
4076 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4077 assert!(
4078 messages
4079 .iter()
4080 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4081 "infinity context alone should trim and notice"
4082 );
4083
4084 let both = vec![
4086 AgentCapabilityConfig {
4087 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4088 config: tight,
4089 },
4090 AgentCapabilityConfig {
4091 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4092 config: serde_json::json!({}),
4093 },
4094 ];
4095 let mut messages = vec![
4096 Message::user("task"),
4097 Message::assistant("old ".repeat(400)),
4098 Message::user("recent"),
4099 ];
4100 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4101 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4102 assert!(
4103 messages
4104 .iter()
4105 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4106 "no hidden-history notice when compaction is the active reducer"
4107 );
4108 }
4109
4110 #[test]
4111 fn test_compaction_is_enabled_detects_compaction() {
4112 let mut registry = CapabilityRegistry::new();
4113 registry.register(CompactionCapability);
4114
4115 let with_compaction = vec![AgentCapabilityConfig {
4116 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4117 config: serde_json::json!({}),
4118 }];
4119 assert!(compaction_is_enabled(&with_compaction, ®istry));
4120
4121 let without = vec![AgentCapabilityConfig {
4122 capability_ref: CapabilityId::new("current_time"),
4123 config: serde_json::json!({}),
4124 }];
4125 assert!(!compaction_is_enabled(&without, ®istry));
4126 }
4127
4128 #[test]
4129 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4130 let registry = CapabilityRegistry::new();
4131
4132 let configs = vec![AgentCapabilityConfig {
4133 capability_ref: CapabilityId::new("nonexistent"),
4134 config: serde_json::json!({}),
4135 }];
4136
4137 let collected = collect_message_filters_only(&configs, ®istry);
4138 assert!(collected.message_filter_providers.is_empty());
4139 }
4140
4141 #[test]
4142 fn test_collect_message_filters_only_preserves_priority_order() {
4143 struct PriorityFilterCap {
4144 id: &'static str,
4145 search_term: &'static str,
4146 priority: i32,
4147 }
4148
4149 struct PriorityFilterProvider {
4150 search_term: &'static str,
4151 priority: i32,
4152 }
4153
4154 impl Capability for PriorityFilterCap {
4155 fn id(&self) -> &str {
4156 self.id
4157 }
4158 fn name(&self) -> &str {
4159 self.id
4160 }
4161 fn description(&self) -> &str {
4162 "priority test"
4163 }
4164 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4165 Some(Arc::new(PriorityFilterProvider {
4166 search_term: self.search_term,
4167 priority: self.priority,
4168 }))
4169 }
4170 }
4171
4172 impl MessageFilterProvider for PriorityFilterProvider {
4173 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4174 query
4175 .filters
4176 .push(MessageFilter::Search(self.search_term.to_string()));
4177 }
4178 fn priority(&self) -> i32 {
4179 self.priority
4180 }
4181 }
4182
4183 let mut registry = CapabilityRegistry::new();
4184 registry.register(PriorityFilterCap {
4185 id: "gamma",
4186 search_term: "gamma",
4187 priority: 10,
4188 });
4189 registry.register(PriorityFilterCap {
4190 id: "alpha",
4191 search_term: "alpha",
4192 priority: 5,
4193 });
4194 registry.register(PriorityFilterCap {
4195 id: "beta",
4196 search_term: "beta",
4197 priority: 1,
4198 });
4199
4200 let configs = vec![
4201 AgentCapabilityConfig {
4202 capability_ref: CapabilityId::new("gamma"),
4203 config: serde_json::json!({}),
4204 },
4205 AgentCapabilityConfig {
4206 capability_ref: CapabilityId::new("alpha"),
4207 config: serde_json::json!({}),
4208 },
4209 AgentCapabilityConfig {
4210 capability_ref: CapabilityId::new("beta"),
4211 config: serde_json::json!({}),
4212 },
4213 ];
4214
4215 let collected = collect_message_filters_only(&configs, ®istry);
4216
4217 let session_id: SessionId = Uuid::now_v7().into();
4218 let mut query = MessageQuery::new(session_id);
4219 collected.apply_message_filters(&mut query);
4220
4221 assert_eq!(query.filters.len(), 3);
4223 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4224 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4225 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4226 }
4227
4228 #[test]
4229 fn test_collect_message_filters_only_post_load_invoked() {
4230 use crate::message::Message;
4231
4232 struct PostLoadCap;
4233 struct PostLoadProvider;
4234
4235 impl Capability for PostLoadCap {
4236 fn id(&self) -> &str {
4237 "post_load_test"
4238 }
4239 fn name(&self) -> &str {
4240 "PostLoad Test"
4241 }
4242 fn description(&self) -> &str {
4243 "test"
4244 }
4245 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4246 Some(Arc::new(PostLoadProvider))
4247 }
4248 }
4249
4250 impl MessageFilterProvider for PostLoadProvider {
4251 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4252 fn priority(&self) -> i32 {
4253 0
4254 }
4255 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4256 messages.reverse();
4258 }
4259 }
4260
4261 let mut registry = CapabilityRegistry::new();
4262 registry.register(PostLoadCap);
4263
4264 let configs = vec![AgentCapabilityConfig {
4265 capability_ref: CapabilityId::new("post_load_test"),
4266 config: serde_json::json!({}),
4267 }];
4268
4269 let collected = collect_message_filters_only(&configs, ®istry);
4270
4271 let mut messages = vec![Message::user("first"), Message::user("second")];
4272 collected.apply_post_load_filters(&mut messages);
4273
4274 assert_eq!(messages[0].text(), Some("second"));
4276 assert_eq!(messages[1].text(), Some("first"));
4277 }
4278
4279 #[test]
4280 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4281 use crate::tool_types::ToolCall;
4282
4283 fn tool_heavy_messages() -> Vec<Message> {
4284 let mut messages = vec![Message::user("inspect files repeatedly")];
4285 for index in 0..9 {
4286 let call_id = format!("call_{index}");
4287 messages.push(Message::assistant_with_tools(
4288 "",
4289 vec![ToolCall {
4290 id: call_id.clone(),
4291 name: "read_file".to_string(),
4292 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4293 }],
4294 ));
4295 messages.push(Message::tool_result(
4296 call_id,
4297 Some(serde_json::json!({
4298 "path": "/workspace/src/lib.rs",
4299 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4300 "total_lines": 1000,
4301 "lines_shown": {"start": 1, "end": 1000},
4302 "truncated": false
4303 })),
4304 None,
4305 ));
4306 }
4307 messages
4308 }
4309
4310 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4311 messages[2]
4312 .tool_result_content()
4313 .and_then(|result| result.result.as_ref())
4314 .and_then(|result| result.get("masked"))
4315 .and_then(|masked| masked.as_bool())
4316 .unwrap_or(false)
4317 }
4318
4319 let mut registry = CapabilityRegistry::new();
4320 registry.register(CompactionCapability);
4321 let context = ModelViewContext {
4322 session_id: SessionId::new(),
4323 prior_usage: None,
4324 };
4325
4326 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4327 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4328 assert!(!first_tool_result_is_masked(&unmasked));
4329
4330 let compaction = collect_model_view_providers(
4331 &[AgentCapabilityConfig {
4332 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4333 config: serde_json::json!({}),
4334 }],
4335 ®istry,
4336 None,
4337 );
4338 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4339 assert!(first_tool_result_is_masked(&masked));
4340 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4341 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4342 }
4343
4344 struct DelegatingFilterCap {
4347 id: &'static str,
4348 inner: std::sync::Arc<InnerFilterCap>,
4349 }
4350 struct InnerFilterCap;
4351
4352 impl Capability for InnerFilterCap {
4353 fn id(&self) -> &str {
4354 "inner_filter"
4355 }
4356 fn name(&self) -> &str {
4357 "Inner Filter"
4358 }
4359 fn description(&self) -> &str {
4360 "inner"
4361 }
4362 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4363 Some(std::sync::Arc::new(SentinelFilter))
4364 }
4365 }
4366 struct SentinelFilter;
4367 impl MessageFilterProvider for SentinelFilter {
4368 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4369 }
4370 impl Capability for DelegatingFilterCap {
4371 fn id(&self) -> &str {
4372 self.id
4373 }
4374 fn name(&self) -> &str {
4375 "Delegating Filter"
4376 }
4377 fn description(&self) -> &str {
4378 "delegating"
4379 }
4380 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4381 None }
4383 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4384 Some(&*self.inner)
4385 }
4386 }
4387
4388 #[test]
4389 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4390 let inner = std::sync::Arc::new(InnerFilterCap);
4391 let outer = DelegatingFilterCap {
4392 id: "delegating_filter",
4393 inner: inner.clone(),
4394 };
4395
4396 let mut registry = CapabilityRegistry::new();
4397 registry.register(outer);
4398
4399 let configs = vec![AgentCapabilityConfig {
4400 capability_ref: CapabilityId::new("delegating_filter"),
4401 config: serde_json::json!({}),
4402 }];
4403
4404 let collected = collect_message_filters_only(&configs, ®istry);
4407 assert_eq!(
4408 collected.message_filter_providers.len(),
4409 1,
4410 "provider from resolved inner capability must be collected"
4411 );
4412 }
4413
4414 struct DelegatingMvpCap {
4415 id: &'static str,
4416 inner: std::sync::Arc<InnerMvpCap>,
4417 }
4418 struct InnerMvpCap;
4419
4420 impl Capability for InnerMvpCap {
4421 fn id(&self) -> &str {
4422 "inner_mvp"
4423 }
4424 fn name(&self) -> &str {
4425 "Inner MVP"
4426 }
4427 fn description(&self) -> &str {
4428 "inner"
4429 }
4430 fn model_view_provider(
4431 &self,
4432 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4433 struct NoopMvp;
4435 impl crate::capabilities::ModelViewProvider for NoopMvp {
4436 fn apply_model_view(
4437 &self,
4438 messages: Vec<Message>,
4439 _config: &serde_json::Value,
4440 _context: &ModelViewContext<'_>,
4441 ) -> Vec<Message> {
4442 messages
4443 }
4444 }
4445 Some(std::sync::Arc::new(NoopMvp))
4446 }
4447 }
4448 impl Capability for DelegatingMvpCap {
4449 fn id(&self) -> &str {
4450 self.id
4451 }
4452 fn name(&self) -> &str {
4453 "Delegating MVP"
4454 }
4455 fn description(&self) -> &str {
4456 "delegating"
4457 }
4458 fn model_view_provider(
4459 &self,
4460 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4461 None }
4463 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4464 Some(&*self.inner)
4465 }
4466 }
4467
4468 #[test]
4469 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4470 let inner = std::sync::Arc::new(InnerMvpCap);
4471 let outer = DelegatingMvpCap {
4472 id: "delegating_mvp",
4473 inner: inner.clone(),
4474 };
4475
4476 let mut registry = CapabilityRegistry::new();
4477 registry.register(outer);
4478
4479 let configs = vec![AgentCapabilityConfig {
4480 capability_ref: CapabilityId::new("delegating_mvp"),
4481 config: serde_json::json!({}),
4482 }];
4483
4484 let collected = collect_model_view_providers(&configs, ®istry, None);
4487 assert_eq!(
4488 collected.model_view_providers.len(),
4489 1,
4490 "provider from resolved inner capability must be collected"
4491 );
4492 }
4493
4494 #[tokio::test]
4504 async fn test_bashkit_shell_capability_produces_bash_tool() {
4505 let registry = CapabilityRegistry::with_builtins();
4506 let collected =
4507 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4508
4509 let tool_names: Vec<&str> = collected
4510 .tool_definitions
4511 .iter()
4512 .map(|t| t.name())
4513 .collect();
4514 assert!(
4515 tool_names.contains(&"bash"),
4516 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4517 tool_names
4518 );
4519 assert!(
4520 !collected.tools.is_empty(),
4521 "bashkit_shell must provide tool implementations"
4522 );
4523 }
4524
4525 #[tokio::test]
4526 async fn test_generic_harness_capability_set_produces_bash_tool() {
4527 let generic_harness_caps = vec![
4530 "session_file_system".to_string(),
4531 "bashkit_shell".to_string(),
4532 "web_fetch".to_string(),
4533 "session_storage".to_string(),
4534 "session".to_string(),
4535 "agent_instructions".to_string(),
4536 "skills".to_string(),
4537 "infinity_context".to_string(),
4538 "auto_tool_search".to_string(),
4539 ];
4540
4541 let registry = CapabilityRegistry::with_builtins();
4542 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4543
4544 let tool_names: Vec<&str> = collected
4545 .tool_definitions
4546 .iter()
4547 .map(|t| t.name())
4548 .collect();
4549 assert!(
4550 tool_names.contains(&"bash"),
4551 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4552 tool_names
4553 );
4554 }
4555
4556 #[tokio::test]
4557 async fn test_collect_capabilities_tool_count_matches_definitions() {
4558 let registry = CapabilityRegistry::with_builtins();
4561 let collected =
4562 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4563
4564 assert_eq!(
4565 collected.tools.len(),
4566 collected.tool_definitions.len(),
4567 "tool implementations ({}) must match tool definitions ({})",
4568 collected.tools.len(),
4569 collected.tool_definitions.len(),
4570 );
4571 }
4572
4573 #[tokio::test]
4577 async fn test_collect_capabilities_resolves_dependencies() {
4578 let registry = CapabilityRegistry::with_builtins();
4581 let collected =
4582 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4583
4584 assert!(
4586 collected
4587 .applied_ids
4588 .iter()
4589 .any(|id| id == "session_file_system"),
4590 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4591 collected.applied_ids
4592 );
4593
4594 let tool_names: Vec<&str> = collected
4595 .tool_definitions
4596 .iter()
4597 .map(|t| t.name())
4598 .collect();
4599
4600 assert!(
4602 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4603 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
4604 tool_names
4605 );
4606
4607 assert_eq!(
4609 collected.tools.len(),
4610 collected.tool_definitions.len(),
4611 "dependency-added tools must have implementations, not just definitions"
4612 );
4613 }
4614
4615 #[test]
4616 fn test_defaults_do_not_include_bash() {
4617 let registry = crate::ToolRegistry::with_defaults();
4620 assert!(
4621 !registry.has("bash"),
4622 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
4623 );
4624 }
4625
4626 #[tokio::test]
4633 async fn test_background_execution_auto_activates_with_bashkit_shell() {
4634 let registry = CapabilityRegistry::with_builtins();
4635 let collected =
4636 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4637
4638 let tool_names: Vec<&str> = collected
4639 .tool_definitions
4640 .iter()
4641 .map(|t| t.name())
4642 .collect();
4643 assert!(
4644 tool_names.contains(&"spawn_background"),
4645 "spawn_background must be auto-activated when bashkit_shell (a \
4646 background-capable tool) is in the agent's capability set; got: {:?}",
4647 tool_names
4648 );
4649 assert!(
4650 collected
4651 .applied_ids
4652 .iter()
4653 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4654 "background_execution must be in applied_ids when auto-activated; \
4655 got: {:?}",
4656 collected.applied_ids
4657 );
4658
4659 assert!(
4661 collected
4662 .tools
4663 .iter()
4664 .any(|t| t.name() == "spawn_background"),
4665 "spawn_background tool implementation must be present alongside the \
4666 definition (lockstep contract)"
4667 );
4668 }
4669
4670 #[tokio::test]
4673 async fn test_background_execution_does_not_auto_activate_without_hint() {
4674 let registry = CapabilityRegistry::with_builtins();
4675 let collected =
4677 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4678
4679 let tool_names: Vec<&str> = collected
4680 .tool_definitions
4681 .iter()
4682 .map(|t| t.name())
4683 .collect();
4684 assert!(
4685 !tool_names.contains(&"spawn_background"),
4686 "spawn_background must NOT be activated without a background-capable \
4687 tool; got: {:?}",
4688 tool_names
4689 );
4690 assert!(
4691 !collected
4692 .applied_ids
4693 .iter()
4694 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
4695 "background_execution must not appear in applied_ids when no \
4696 background-capable tool is present; got: {:?}",
4697 collected.applied_ids
4698 );
4699 }
4700
4701 #[tokio::test]
4705 async fn test_background_execution_explicit_selection_is_idempotent() {
4706 let registry = CapabilityRegistry::with_builtins();
4707 let collected = collect_capabilities(
4708 &[
4709 "bashkit_shell".to_string(),
4710 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
4711 ],
4712 ®istry,
4713 &test_ctx(),
4714 )
4715 .await;
4716
4717 let spawn_background_count = collected
4718 .tool_definitions
4719 .iter()
4720 .filter(|t| t.name() == "spawn_background")
4721 .count();
4722 assert_eq!(
4723 spawn_background_count, 1,
4724 "spawn_background must appear exactly once even when \
4725 background_execution is selected explicitly alongside a \
4726 background-capable tool"
4727 );
4728 let applied_count = collected
4729 .applied_ids
4730 .iter()
4731 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
4732 .count();
4733 assert_eq!(
4734 applied_count, 1,
4735 "background_execution must appear exactly once in applied_ids"
4736 );
4737 }
4738
4739 #[test]
4744 fn test_defaults_do_not_include_spawn_background() {
4745 let registry = crate::ToolRegistry::with_defaults();
4746 assert!(
4747 !registry.has("spawn_background"),
4748 "with_defaults() must not include 'spawn_background' — it comes \
4749 from the background_execution capability (EVE-501)"
4750 );
4751 }
4752
4753 #[test]
4758 fn test_capability_features_default_empty() {
4759 let registry = CapabilityRegistry::with_builtins();
4760
4761 let noop = registry.get("noop").unwrap();
4763 assert!(noop.features().is_empty());
4764
4765 let current_time = registry.get("current_time").unwrap();
4766 assert!(current_time.features().is_empty());
4767 }
4768
4769 #[test]
4770 fn test_file_system_capability_features() {
4771 let registry = CapabilityRegistry::with_builtins();
4772
4773 let fs = registry.get("session_file_system").unwrap();
4774 assert_eq!(fs.features(), vec!["file_system"]);
4775 }
4776
4777 #[test]
4778 fn test_bashkit_shell_capability_features() {
4779 let registry = CapabilityRegistry::with_builtins();
4780
4781 let bash = registry.get("bashkit_shell").unwrap();
4782 assert_eq!(bash.features(), vec!["file_system"]);
4783 }
4784
4785 #[test]
4786 fn test_alias_resolves_to_canonical_capability() {
4787 let registry = CapabilityRegistry::with_builtins();
4788
4789 let via_alias = registry.get("virtual_bash").unwrap();
4791 assert_eq!(via_alias.id(), "bashkit_shell");
4792 assert!(registry.has("virtual_bash"));
4793 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
4794 assert_eq!(
4795 registry.canonical_id("bashkit_shell"),
4796 Some("bashkit_shell")
4797 );
4798 assert_eq!(registry.canonical_id("nonexistent"), None);
4799 }
4800
4801 #[test]
4802 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
4803 let registry = CapabilityRegistry::with_builtins();
4804
4805 let resolved = resolve_dependencies(
4808 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
4809 ®istry,
4810 )
4811 .unwrap();
4812 let bash_ids: Vec<_> = resolved
4813 .resolved_ids
4814 .iter()
4815 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
4816 .collect();
4817 assert_eq!(bash_ids, vec!["bashkit_shell"]);
4818 assert!(
4820 !resolved
4821 .added_as_dependencies
4822 .contains(&"bashkit_shell".to_string())
4823 );
4824 }
4825
4826 #[test]
4827 fn test_alias_preserves_explicit_config_in_resolution() {
4828 let registry = CapabilityRegistry::with_builtins();
4829
4830 let configs = vec![AgentCapabilityConfig::with_config(
4831 "virtual_bash".to_string(),
4832 serde_json::json!({"key": "value"}),
4833 )];
4834 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
4835 let bash = resolved
4836 .iter()
4837 .find(|c| c.capability_id() == "bashkit_shell")
4838 .expect("alias must resolve to canonical bashkit_shell config");
4839 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
4840 }
4841
4842 #[test]
4843 fn test_unregister_by_alias_removes_capability_and_aliases() {
4844 let mut registry = CapabilityRegistry::with_builtins();
4845
4846 assert!(registry.unregister("virtual_bash").is_some());
4847 assert!(!registry.has("bashkit_shell"));
4848 assert!(!registry.has("virtual_bash"));
4849 }
4850
4851 #[test]
4852 fn test_session_storage_capability_features() {
4853 let registry = CapabilityRegistry::with_builtins();
4854
4855 let storage = registry.get("session_storage").unwrap();
4856 let features = storage.features();
4857 assert!(features.contains(&"secrets"));
4858 assert!(features.contains(&"key_value"));
4859 }
4860
4861 #[test]
4862 fn test_session_schedule_capability_features() {
4863 let registry = CapabilityRegistry::with_builtins();
4864
4865 let schedule = registry.get("session_schedule").unwrap();
4866 assert_eq!(schedule.features(), vec!["schedules"]);
4867 }
4868
4869 #[test]
4870 fn test_session_sql_database_capability_features() {
4871 let registry = CapabilityRegistry::with_builtins();
4872
4873 let sql = registry.get("session_sql_database").unwrap();
4874 assert_eq!(sql.features(), vec!["sql_database"]);
4875 }
4876
4877 #[test]
4878 fn test_sample_data_capability_features() {
4879 let registry = CapabilityRegistry::with_builtins();
4880
4881 let sample = registry.get("sample_data").unwrap();
4882 assert_eq!(sample.features(), vec!["file_system"]);
4883 }
4884
4885 #[test]
4886 fn test_compute_features_empty() {
4887 let registry = CapabilityRegistry::with_builtins();
4888
4889 let features = compute_features(&[], ®istry);
4890 assert!(features.is_empty());
4891 }
4892
4893 #[test]
4894 fn test_compute_features_single_capability() {
4895 let registry = CapabilityRegistry::with_builtins();
4896
4897 let features = compute_features(&["session_schedule".to_string()], ®istry);
4898 assert_eq!(features, vec!["schedules"]);
4899 }
4900
4901 #[test]
4902 fn test_compute_features_multiple_capabilities() {
4903 let registry = CapabilityRegistry::with_builtins();
4904
4905 let features = compute_features(
4906 &[
4907 "session_file_system".to_string(),
4908 "session_storage".to_string(),
4909 "session_schedule".to_string(),
4910 ],
4911 ®istry,
4912 );
4913 assert!(features.contains(&"file_system".to_string()));
4914 assert!(features.contains(&"secrets".to_string()));
4915 assert!(features.contains(&"key_value".to_string()));
4916 assert!(features.contains(&"schedules".to_string()));
4917 }
4918
4919 #[test]
4920 fn test_compute_features_deduplicates() {
4921 let registry = CapabilityRegistry::with_builtins();
4922
4923 let features = compute_features(
4925 &[
4926 "session_file_system".to_string(),
4927 "bashkit_shell".to_string(),
4928 ],
4929 ®istry,
4930 );
4931 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
4932 assert_eq!(file_system_count, 1, "file_system should appear only once");
4933 }
4934
4935 #[test]
4936 fn test_compute_features_includes_dependency_features() {
4937 let registry = CapabilityRegistry::with_builtins();
4938
4939 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
4941 assert!(features.contains(&"file_system".to_string()));
4942 }
4943
4944 #[test]
4945 fn test_compute_features_generic_harness_set() {
4946 let registry = CapabilityRegistry::with_builtins();
4947
4948 let features = compute_features(
4950 &[
4951 "session_file_system".to_string(),
4952 "bashkit_shell".to_string(),
4953 "session_storage".to_string(),
4954 "session".to_string(),
4955 "session_schedule".to_string(),
4956 ],
4957 ®istry,
4958 );
4959 assert!(features.contains(&"file_system".to_string()));
4960 assert!(features.contains(&"secrets".to_string()));
4961 assert!(features.contains(&"key_value".to_string()));
4962 assert!(features.contains(&"schedules".to_string()));
4963 }
4964
4965 #[test]
4966 fn test_compute_features_unknown_capability_ignored() {
4967 let registry = CapabilityRegistry::with_builtins();
4968
4969 let features = compute_features(
4970 &["unknown_cap".to_string(), "session_schedule".to_string()],
4971 ®istry,
4972 );
4973 assert_eq!(features, vec!["schedules"]);
4974 }
4975
4976 #[test]
4977 fn test_risk_level_ordering() {
4978 assert!(RiskLevel::Low < RiskLevel::Medium);
4979 assert!(RiskLevel::Medium < RiskLevel::High);
4980 }
4981
4982 #[test]
4983 fn test_risk_level_serde_roundtrip() {
4984 let high = RiskLevel::High;
4985 let json = serde_json::to_string(&high).unwrap();
4986 assert_eq!(json, "\"high\"");
4987 let back: RiskLevel = serde_json::from_str(&json).unwrap();
4988 assert_eq!(back, RiskLevel::High);
4989 }
4990
4991 #[test]
4992 fn test_capability_risk_levels() {
4993 let registry = CapabilityRegistry::with_builtins();
4994
4995 let bash = registry.get("bashkit_shell").unwrap();
4997 assert_eq!(bash.risk_level(), RiskLevel::High);
4998
4999 let fetch = registry.get("web_fetch").unwrap();
5001 assert_eq!(fetch.risk_level(), RiskLevel::High);
5002
5003 let noop = registry.get("noop").unwrap();
5005 assert_eq!(noop.risk_level(), RiskLevel::Low);
5006 }
5007
5008 #[tokio::test]
5013 async fn test_apply_capabilities_openai_tool_search() {
5014 let registry = CapabilityRegistry::with_builtins();
5015 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5016
5017 let applied = apply_capabilities(
5018 base_runtime_agent.clone(),
5019 &["openai_tool_search".to_string()],
5020 ®istry,
5021 &test_ctx(),
5022 )
5023 .await;
5024
5025 assert_eq!(
5027 applied.runtime_agent.system_prompt,
5028 base_runtime_agent.system_prompt
5029 );
5030 assert!(applied.tool_registry.is_empty());
5031 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
5032
5033 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5035 assert!(ts.enabled);
5036 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5037 }
5038
5039 #[tokio::test]
5040 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
5041 let registry = CapabilityRegistry::with_builtins();
5042 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5043
5044 let applied = apply_capabilities(
5045 base_runtime_agent,
5046 &[
5047 "current_time".to_string(),
5048 "openai_tool_search".to_string(),
5049 "test_math".to_string(),
5050 ],
5051 ®istry,
5052 &test_ctx(),
5053 )
5054 .await;
5055
5056 assert!(applied.tool_registry.has("get_current_time"));
5058 assert!(applied.tool_registry.has("add"));
5059 assert!(applied.tool_registry.has("subtract"));
5060 assert!(applied.tool_registry.has("multiply"));
5061 assert!(applied.tool_registry.has("divide"));
5062
5063 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5065 assert!(ts.enabled);
5066 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5067 }
5068
5069 #[tokio::test]
5070 async fn test_collect_capabilities_tool_search_custom_threshold() {
5071 let registry = CapabilityRegistry::with_builtins();
5072
5073 let configs = vec![AgentCapabilityConfig {
5074 capability_ref: CapabilityId::new("openai_tool_search"),
5075 config: serde_json::json!({"threshold": 5}),
5076 }];
5077
5078 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5079
5080 let ts = collected.tool_search.as_ref().unwrap();
5081 assert!(ts.enabled);
5082 assert_eq!(ts.threshold, 5);
5083 }
5084
5085 #[tokio::test]
5086 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
5087 let registry = CapabilityRegistry::with_builtins();
5088
5089 let configs = vec![
5090 AgentCapabilityConfig {
5091 capability_ref: CapabilityId::new("auto_tool_search"),
5092 config: serde_json::json!({"threshold": 2}),
5093 },
5094 AgentCapabilityConfig {
5095 capability_ref: CapabilityId::new("test_math"),
5096 config: serde_json::json!({}),
5097 },
5098 ];
5099
5100 let ctx = test_ctx().with_model("claude-3-5-haiku");
5104 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5105
5106 assert!(
5107 collected.tool_search.is_none(),
5108 "auto_tool_search must not set a hosted config on a non-native model"
5109 );
5110 assert!(
5111 collected
5112 .tools
5113 .iter()
5114 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5115 "auto_tool_search must contribute the client-side tool_search tool"
5116 );
5117 assert!(
5118 !collected.tool_definition_hooks.is_empty(),
5119 "auto_tool_search must contribute a client-side deferral hook"
5120 );
5121
5122 let mut transformed = collected.tool_definitions.clone();
5123 for hook in &collected.tool_definition_hooks {
5124 transformed = hook.transform(transformed);
5125 }
5126 let add_tool = transformed
5127 .iter()
5128 .find(|tool| tool.name() == "add")
5129 .expect("test_math contributes add");
5130 assert!(
5131 add_tool.parameters().get("properties").is_none(),
5132 "generic auto_tool_search must honor the configured threshold"
5133 );
5134 }
5135
5136 #[tokio::test]
5137 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
5138 let registry = CapabilityRegistry::with_builtins();
5139
5140 let configs = vec![AgentCapabilityConfig {
5141 capability_ref: CapabilityId::new("auto_tool_search"),
5142 config: serde_json::json!({"threshold": 7}),
5143 }];
5144
5145 let ctx = test_ctx().with_model("gpt-5.4");
5148 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5149
5150 let ts = collected
5151 .tool_search
5152 .as_ref()
5153 .expect("auto_tool_search must set a hosted config on a native model");
5154 assert!(ts.enabled);
5155 assert_eq!(ts.threshold, 7);
5156 assert!(
5157 !collected
5158 .tools
5159 .iter()
5160 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5161 "hosted mechanism must not contribute the client-side tool_search tool"
5162 );
5163 assert!(
5164 collected.tool_definition_hooks.is_empty(),
5165 "hosted mechanism must not contribute a client-side deferral hook"
5166 );
5167 }
5168
5169 #[tokio::test]
5170 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
5171 let registry = CapabilityRegistry::with_builtins();
5172
5173 let configs = vec![AgentCapabilityConfig {
5174 capability_ref: CapabilityId::new("auto_tool_search"),
5175 config: serde_json::json!({"threshold": 9}),
5176 }];
5177
5178 let ctx = test_ctx().with_model("claude-opus-4-8");
5181 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5182
5183 let ts = collected
5184 .tool_search
5185 .as_ref()
5186 .expect("auto_tool_search must set a hosted config on a native Claude model");
5187 assert!(ts.enabled);
5188 assert_eq!(ts.threshold, 9);
5189 assert!(
5190 !collected
5191 .tools
5192 .iter()
5193 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5194 "hosted mechanism must not contribute the client-side tool_search tool"
5195 );
5196 assert!(
5197 collected.tool_definition_hooks.is_empty(),
5198 "hosted mechanism must not contribute a client-side deferral hook"
5199 );
5200 }
5201
5202 #[tokio::test]
5203 async fn test_collect_capabilities_no_tool_search_without_capability() {
5204 let registry = CapabilityRegistry::with_builtins();
5205
5206 let configs = vec![AgentCapabilityConfig {
5207 capability_ref: CapabilityId::new("current_time"),
5208 config: serde_json::json!({}),
5209 }];
5210
5211 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5212
5213 assert!(collected.tool_search.is_none());
5214 }
5215
5216 #[tokio::test]
5217 async fn test_collect_capabilities_tool_search_category_propagation() {
5218 let registry = CapabilityRegistry::with_builtins();
5219
5220 let configs = vec![
5222 AgentCapabilityConfig {
5223 capability_ref: CapabilityId::new("test_math"),
5224 config: serde_json::json!({}),
5225 },
5226 AgentCapabilityConfig {
5227 capability_ref: CapabilityId::new("openai_tool_search"),
5228 config: serde_json::json!({}),
5229 },
5230 ];
5231
5232 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5233
5234 assert!(collected.tool_search.is_some());
5236
5237 for tool_def in &collected.tool_definitions {
5239 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
5241 assert!(
5242 tool_def.category().is_some(),
5243 "Tool {} should have a category from its capability",
5244 tool_def.name()
5245 );
5246 }
5247 }
5248 }
5249
5250 #[tokio::test]
5251 async fn test_apply_capabilities_prompt_caching() {
5252 let registry = CapabilityRegistry::with_builtins();
5253 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5254
5255 let applied = apply_capabilities(
5256 base_runtime_agent.clone(),
5257 &["prompt_caching".to_string()],
5258 ®istry,
5259 &test_ctx(),
5260 )
5261 .await;
5262
5263 assert_eq!(
5264 applied.runtime_agent.system_prompt,
5265 base_runtime_agent.system_prompt
5266 );
5267 assert!(applied.tool_registry.is_empty());
5268 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
5269
5270 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
5271 assert!(prompt_cache.enabled);
5272 assert_eq!(
5273 prompt_cache.strategy,
5274 crate::driver_registry::PromptCacheStrategy::Auto
5275 );
5276 assert!(prompt_cache.gemini_cached_content.is_none());
5277 }
5278
5279 #[tokio::test]
5280 async fn test_apply_capabilities_openrouter_server_tools() {
5281 let registry = CapabilityRegistry::with_builtins();
5282 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5283
5284 let configs = vec![AgentCapabilityConfig {
5285 capability_ref: CapabilityId::new("openrouter_server_tools"),
5286 config: serde_json::json!({
5287 "tools": ["web_search", "datetime"],
5288 "web_search_max_results": 4,
5289 }),
5290 }];
5291
5292 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5293 let routing = collected
5294 .openrouter_routing
5295 .as_ref()
5296 .expect("server tools produce routing config");
5297 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
5298 assert_eq!(
5299 kinds,
5300 vec![
5301 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
5302 crate::driver_registry::OpenRouterServerToolKind::Datetime,
5303 ]
5304 );
5305
5306 let applied = apply_capabilities(
5309 base_runtime_agent,
5310 &["openrouter_server_tools".to_string()],
5311 ®istry,
5312 &test_ctx(),
5313 )
5314 .await;
5315 assert!(applied.tool_registry.is_empty());
5316 assert!(applied.runtime_agent.openrouter_routing.is_none());
5317 }
5318
5319 #[tokio::test]
5320 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
5321 let registry = CapabilityRegistry::with_builtins();
5322
5323 let configs = vec![AgentCapabilityConfig {
5324 capability_ref: CapabilityId::new("prompt_caching"),
5325 config: serde_json::json!({"strategy": "auto"}),
5326 }];
5327
5328 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5329
5330 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5331 assert!(prompt_cache.enabled);
5332 assert_eq!(
5333 prompt_cache.strategy,
5334 crate::driver_registry::PromptCacheStrategy::Auto
5335 );
5336 assert!(prompt_cache.gemini_cached_content.is_none());
5337 }
5338
5339 #[tokio::test]
5340 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
5341 let registry = CapabilityRegistry::with_builtins();
5342
5343 let configs = vec![AgentCapabilityConfig {
5344 capability_ref: CapabilityId::new("prompt_caching"),
5345 config: serde_json::json!({
5346 "strategy": "auto",
5347 "gemini_cached_content": "cachedContents/demo-cache"
5348 }),
5349 }];
5350
5351 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5352
5353 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5354 assert_eq!(
5355 prompt_cache.gemini_cached_content.as_deref(),
5356 Some("cachedContents/demo-cache")
5357 );
5358 }
5359
5360 #[tokio::test]
5361 async fn test_collect_capabilities_parallel_tool_calls_modes() {
5362 let registry = CapabilityRegistry::with_builtins();
5363
5364 let collected = collect_capabilities_with_configs(
5366 &[AgentCapabilityConfig::new("parallel_tool_calls")],
5367 ®istry,
5368 &test_ctx(),
5369 )
5370 .await;
5371 assert_eq!(collected.parallel_tool_calls, Some(true));
5372
5373 let collected = collect_capabilities_with_configs(
5375 &[AgentCapabilityConfig {
5376 capability_ref: CapabilityId::new("parallel_tool_calls"),
5377 config: serde_json::json!({"mode": "avoid"}),
5378 }],
5379 ®istry,
5380 &test_ctx(),
5381 )
5382 .await;
5383 assert_eq!(collected.parallel_tool_calls, Some(false));
5384
5385 let collected = collect_capabilities_with_configs(
5387 &[AgentCapabilityConfig {
5388 capability_ref: CapabilityId::new("parallel_tool_calls"),
5389 config: serde_json::json!({"mode": "none"}),
5390 }],
5391 ®istry,
5392 &test_ctx(),
5393 )
5394 .await;
5395 assert_eq!(collected.parallel_tool_calls, None);
5396
5397 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
5399 assert_eq!(collected.parallel_tool_calls, None);
5400 }
5401
5402 #[tokio::test]
5403 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
5404 let registry = CapabilityRegistry::with_builtins();
5405
5406 let applied = apply_capabilities(
5408 RuntimeAgent::new("p", "gpt-5.2"),
5409 &["parallel_tool_calls".to_string()],
5410 ®istry,
5411 &test_ctx(),
5412 )
5413 .await;
5414 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
5415
5416 let mut base = RuntimeAgent::new("p", "gpt-5.2");
5418 base.parallel_tool_calls = Some(false);
5419 let applied = apply_capabilities(
5420 base,
5421 &["parallel_tool_calls".to_string()],
5422 ®istry,
5423 &test_ctx(),
5424 )
5425 .await;
5426 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
5427 }
5428
5429 struct SkillContributingCapability;
5434
5435 impl Capability for SkillContributingCapability {
5436 fn id(&self) -> &str {
5437 "contributes_skills"
5438 }
5439 fn name(&self) -> &str {
5440 "Contributes Skills"
5441 }
5442 fn description(&self) -> &str {
5443 "Test capability that contributes skills."
5444 }
5445 fn contribute_skills(&self) -> Vec<SkillContribution> {
5446 vec![
5447 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
5448 .with_files(vec![(
5449 "scripts/a.sh".to_string(),
5450 "#!/bin/sh\necho a\n".to_string(),
5451 )]),
5452 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
5453 .with_user_invocable(false),
5454 ]
5455 }
5456 }
5457
5458 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
5459 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
5460 MountSource::InlineFile { content, .. } => content.as_str(),
5461 _ => panic!("Expected InlineFile for SKILL.md"),
5462 }
5463 }
5464
5465 #[tokio::test]
5466 async fn test_contribute_skills_normalized_to_mounts() {
5467 let mut registry = CapabilityRegistry::new();
5468 registry.register(SkillContributingCapability);
5469
5470 let configs = vec![AgentCapabilityConfig {
5471 capability_ref: CapabilityId::new("contributes_skills"),
5472 config: serde_json::json!({}),
5473 }];
5474
5475 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5476
5477 let skill_mounts: Vec<_> = collected
5478 .mounts
5479 .iter()
5480 .filter(|m| m.path.starts_with("/.agents/skills/"))
5481 .collect();
5482 assert_eq!(skill_mounts.len(), 2);
5483
5484 for m in &skill_mounts {
5487 assert!(m.is_readonly());
5488 assert_eq!(m.capability_id, "contributes_skills");
5489 }
5490
5491 let alpha = skill_mounts
5492 .iter()
5493 .find(|m| m.path == "/.agents/skills/alpha-skill")
5494 .expect("alpha-skill mount missing");
5495 match &alpha.source {
5496 MountSource::InlineDirectory { entries } => {
5497 assert!(entries.contains_key("SKILL.md"));
5498 assert!(entries.contains_key("scripts/a.sh"));
5499 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5500 assert_eq!(parsed.name, "alpha-skill");
5501 assert!(parsed.user_invocable);
5502 }
5503 _ => panic!("Expected InlineDirectory"),
5504 }
5505
5506 let beta = skill_mounts
5507 .iter()
5508 .find(|m| m.path == "/.agents/skills/beta-skill")
5509 .expect("beta-skill mount missing");
5510 match &beta.source {
5511 MountSource::InlineDirectory { entries } => {
5512 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
5513 assert!(!parsed.user_invocable);
5514 }
5515 _ => panic!("Expected InlineDirectory"),
5516 }
5517 }
5518
5519 #[tokio::test]
5520 async fn test_contribute_skills_default_empty() {
5521 let mut registry = CapabilityRegistry::new();
5524 registry.register(FilterTestCapability { priority: 0 });
5525
5526 let configs = vec![AgentCapabilityConfig {
5527 capability_ref: CapabilityId::new("filter_test"),
5528 config: serde_json::json!({}),
5529 }];
5530
5531 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5532 assert!(
5533 collected
5534 .mounts
5535 .iter()
5536 .all(|m| !m.path.starts_with("/.agents/skills/"))
5537 );
5538 }
5539
5540 struct LocalizedCapability;
5541
5542 impl Capability for LocalizedCapability {
5543 fn id(&self) -> &str {
5544 "localized"
5545 }
5546 fn name(&self) -> &str {
5547 "Localized"
5548 }
5549 fn description(&self) -> &str {
5550 "English description"
5551 }
5552 fn localizations(&self) -> Vec<CapabilityLocalization> {
5553 vec![
5554 CapabilityLocalization {
5555 locale: "en",
5556 name: None,
5557 description: None,
5558 config_description: Some("Controls things."),
5559 config_overlay: None,
5560 },
5561 CapabilityLocalization {
5562 locale: "uk",
5563 name: Some("Локалізована"),
5564 description: Some("Український опис"),
5565 config_description: Some("Керує налаштуваннями."),
5566 config_overlay: None,
5567 },
5568 ]
5569 }
5570 }
5571
5572 #[test]
5573 fn localized_name_falls_back_exact_language_then_base() {
5574 let cap = LocalizedCapability;
5575 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
5577 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
5578 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
5580 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
5582 assert_eq!(cap.localized_name(None), "Localized");
5583 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
5584 assert_eq!(cap.localized_description(Some("de")), "English description");
5585 }
5586
5587 #[test]
5588 fn describe_schema_resolves_config_description_per_locale() {
5589 let cap = LocalizedCapability;
5590 assert_eq!(
5591 cap.describe_schema(Some("uk-UA")).as_deref(),
5592 Some("Керує налаштуваннями.")
5593 );
5594 assert_eq!(
5596 cap.describe_schema(Some("pl")).as_deref(),
5597 Some("Controls things.")
5598 );
5599 assert_eq!(
5600 cap.describe_schema(None).as_deref(),
5601 Some("Controls things.")
5602 );
5603 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
5605 }
5606}