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