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, ToolExecutionResult, ToolRegistry};
33use crate::traits::{SessionFileSystem, ToolContext};
34use crate::typed_id::SessionId;
35use async_trait::async_trait;
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::sync::Arc;
39
40pub struct IntegrationPlugin {
64 pub experimental_only: bool,
66 pub feature_flag: Option<&'static str>,
69 pub factory: fn() -> Box<dyn Capability>,
71}
72
73inventory::collect!(IntegrationPlugin);
74
75pub use crate::capability_types::{
77 AgentCapabilityConfig, CapabilityId, CapabilityStatus, MountAccess, MountDirectoryBuilder,
78 MountEntry, MountPoint, MountSource,
79};
80
81#[cfg(feature = "a2a")]
86mod a2a_delegation;
87#[cfg(feature = "ui-capabilities")]
88mod a2ui;
89mod agent_handoff;
90mod agent_instructions;
91pub mod attach_skill;
92mod auto_tool_search;
93mod background_execution;
94mod bashkit_shell;
95mod btw;
96mod budgeting;
97mod claude_tool_search;
98pub mod compaction;
99mod current_time;
100mod data_knowledge;
101mod declarative;
102mod error_disclosure;
103pub mod facts;
104mod fake_aws;
105mod fake_crm;
106mod fake_financial;
107mod fake_warehouse;
108mod file_system;
109mod guardrails;
110mod human_intent;
111mod infinity_context;
112mod knowledge_base;
113mod knowledge_index;
114mod loop_detection;
115mod lua;
116mod lua_code_mode;
117pub mod mcp;
118mod memory;
119mod message_metadata;
120mod model_scout;
121mod monitors;
122mod noop;
123mod openai_tool_search;
124mod openrouter_server_tools;
125mod openrouter_workspace;
126#[cfg(feature = "ui-capabilities")]
127mod openui;
128mod parallel_tool_calls;
129mod platform_management;
130mod prompt_caching;
131mod prompt_canary_guardrail;
132mod research;
133mod sample_data;
134mod self_budget;
135mod session;
136mod session_sandbox;
137mod session_schedule;
138mod session_sql_database;
139mod session_storage;
140mod session_tasks;
141mod skills;
142mod skills_scoped;
143mod stateless_todo_list;
144mod subagents;
145mod system_commands;
146mod test_math;
147mod test_weather;
148mod tool_call_repair;
149mod tool_output_distillation;
150mod tool_output_persistence;
151mod tool_search;
152pub mod user_hooks;
153mod util;
154#[cfg(feature = "web-fetch")]
155mod web_fetch;
156
157pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
162pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
166#[cfg(feature = "a2a")]
167pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
168#[cfg(feature = "ui-capabilities")]
169pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
170pub use agent_handoff::{
171 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
172};
173pub use agent_instructions::{
174 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
175 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
176 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
177};
178pub use attach_skill::{
179 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
180 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
181 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
182};
183pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
184pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
185pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
186pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
187pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
188pub use compaction::{
189 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
190 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
191 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
192 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
193 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
194 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
195 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
196};
197pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
198pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
199pub use declarative::{
200 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
201 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
202 declarative_capability_info, hydrate_declarative_capability_config,
203 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
204 plugin_capability_info, validate_declarative_capability_definition,
205};
206pub use error_disclosure::{
207 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
208};
209pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
210pub use fake_aws::{
211 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
212 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
213 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
214 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
215};
216pub use fake_crm::{
217 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
218 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
219 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
220};
221pub use fake_financial::{
222 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
223 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
224 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
225 FinanceListTransactionsTool,
226};
227pub use fake_warehouse::{
228 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
229 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
230 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
231 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
232};
233pub use file_system::{
234 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
235 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
236};
237pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
238pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
239pub use infinity_context::{
240 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
241};
242pub use knowledge_base::{
243 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
244 validate_knowledge_base_config,
245};
246pub use knowledge_index::{
247 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
248 validate_knowledge_index_config,
249};
250pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
251pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
252pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
253pub use mcp::{
254 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
255 parse_mcp_capability_id,
256};
257pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
258pub use message_metadata::{
259 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
260 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
261};
262pub use model_scout::{
263 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
264 RouterUpdateProposal, compute_score, rank_results,
265};
266pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
267pub use openai_tool_search::{
268 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
269 model_supports_native_tool_search,
270};
271pub use openrouter_server_tools::{
272 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
273};
274pub use openrouter_workspace::{
275 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
276 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
277 detect_policy_drift,
278};
279#[cfg(feature = "ui-capabilities")]
280pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
281pub use parallel_tool_calls::{
282 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
283 parallel_tool_calls_from_config,
284};
285pub use platform_management::{
286 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
287 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
288 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
289};
290pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
291pub use prompt_canary_guardrail::{
292 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
293 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
294 REASON_CODE_SYSTEM_PROMPT_LEAK,
295};
296pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
297pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
298pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
299pub use session::{
300 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
301};
302pub use session_sandbox::{
303 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
304 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
305};
306pub use session_schedule::{
307 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
308 SessionScheduleCapability,
309};
310pub use session_sql_database::{
311 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
312 SqlSchemaTool,
313};
314pub use session_storage::{
315 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
316 is_internal_session_kv_key,
317};
318pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
319pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
320pub use skills_scoped::{
321 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
322};
323pub use stateless_todo_list::{
324 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
325};
326pub use subagents::{
327 ReportResultTool, ReportTaskProgressTool, SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool,
328 SubagentCapability, report_result_tool_for_child_session,
329 report_task_progress_tool_for_child_session,
330};
331pub use bashkit_shell::{
333 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
334};
335pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
336pub use test_math::{
337 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
338};
339pub use test_weather::{
340 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
341};
342pub use tool_call_repair::{
343 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
344 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
345 salvage_tool_arguments, tool_call_repair_capability,
346};
347pub use tool_output_distillation::{
348 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
349};
350pub use tool_output_persistence::{
351 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
352};
353pub use tool_search::{
354 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
355};
356pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
357#[cfg(feature = "web-fetch")]
358pub use web_fetch::{
359 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
360 derive_bot_auth_public_key,
361};
362
363pub struct SystemPromptContext {
373 pub session_id: SessionId,
375 pub locale: Option<String>,
377 pub file_store: Option<Arc<dyn SessionFileSystem>>,
379 pub model: Option<String>,
385}
386
387impl SystemPromptContext {
388 pub fn without_file_store(session_id: SessionId) -> Self {
390 Self {
391 session_id,
392 locale: None,
393 file_store: None,
394 model: None,
395 }
396 }
397
398 pub fn with_model(mut self, model: impl Into<String>) -> Self {
400 self.model = Some(model.into());
401 self
402 }
403}
404
405#[derive(Debug, Clone)]
457pub struct CapabilityLocalization {
458 pub locale: &'static str,
460 pub name: Option<&'static str>,
462 pub description: Option<&'static str>,
464 pub config_description: Option<&'static str>,
469 pub config_overlay: Option<serde_json::Value>,
475}
476
477impl CapabilityLocalization {
478 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
480 Self {
481 locale,
482 name: Some(name),
483 description: Some(description),
484 config_description: None,
485 config_overlay: None,
486 }
487 }
488}
489
490pub fn resolve_localized_field<T>(
494 localizations: &[CapabilityLocalization],
495 locale: Option<&str>,
496 field: impl Fn(&CapabilityLocalization) -> Option<T>,
497) -> Option<T> {
498 let mut candidates: Vec<String> = Vec::new();
499 if let Some(raw) = locale {
500 let normalized = raw.trim().replace('_', "-").to_lowercase();
501 if !normalized.is_empty() {
502 if let Some((language, _)) = normalized.split_once('-') {
503 let language = language.to_string();
504 candidates.push(normalized);
505 candidates.push(language);
506 } else {
507 candidates.push(normalized);
508 }
509 }
510 }
511 candidates.push("en".to_string());
512
513 for candidate in candidates {
514 let hit = localizations
515 .iter()
516 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
517 .and_then(&field);
518 if hit.is_some() {
519 return hit;
520 }
521 }
522 None
523}
524
525#[async_trait]
526pub trait Capability: Send + Sync {
527 fn id(&self) -> &str;
529
530 fn aliases(&self) -> Vec<&'static str> {
539 vec![]
540 }
541
542 fn name(&self) -> &str;
544
545 fn description(&self) -> &str;
547
548 fn localizations(&self) -> Vec<CapabilityLocalization> {
553 vec![]
554 }
555
556 fn localized_name(&self, locale: Option<&str>) -> String {
559 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
560 .unwrap_or_else(|| self.name())
561 .to_string()
562 }
563
564 fn localized_description(&self, locale: Option<&str>) -> String {
566 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
567 .unwrap_or_else(|| self.description())
568 .to_string()
569 }
570
571 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
575 resolve_localized_field(&self.localizations(), locale, |entry| {
576 entry.config_description
577 })
578 .map(str::to_string)
579 }
580
581 fn status(&self) -> CapabilityStatus {
583 CapabilityStatus::Available
584 }
585
586 fn icon(&self) -> Option<&str> {
588 None
589 }
590
591 fn category(&self) -> Option<&str> {
593 None
594 }
595
596 fn is_guardrail(&self) -> bool {
601 false
602 }
603
604 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
615 None
616 }
617
618 fn system_prompt_addition(&self) -> Option<&str> {
638 None
639 }
640
641 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
653 self.system_prompt_addition().map(|addition| {
654 format!(
655 "<capability id=\"{}\">\n{}\n</capability>",
656 self.id(),
657 addition
658 )
659 })
660 }
661
662 fn system_prompt_preview(&self) -> Option<String> {
668 self.system_prompt_addition().map(|s| s.to_string())
669 }
670
671 fn tools(&self) -> Vec<Box<dyn Tool>> {
673 vec![]
674 }
675
676 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
684 self.tools()
685 }
686
687 async fn system_prompt_contribution_with_config(
694 &self,
695 ctx: &SystemPromptContext,
696 _config: &serde_json::Value,
697 ) -> Option<String> {
698 self.system_prompt_contribution(ctx).await
699 }
700
701 fn tool_definitions(&self) -> Vec<ToolDefinition> {
704 self.tools().iter().map(|t| t.to_definition()).collect()
705 }
706
707 fn mounts(&self) -> Vec<MountPoint> {
715 vec![]
716 }
717
718 fn dependencies(&self) -> Vec<&'static str> {
727 vec![]
728 }
729
730 fn features(&self) -> Vec<&'static str> {
745 vec![]
746 }
747
748 fn config_schema(&self) -> Option<serde_json::Value> {
754 None
755 }
756
757 fn config_ui_schema(&self) -> Option<serde_json::Value> {
762 None
763 }
764
765 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
771 Ok(())
772 }
773
774 fn mcp_servers(&self) -> ScopedMcpServers {
780 ScopedMcpServers::default()
781 }
782
783 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
785 self.mcp_servers()
786 }
787
788 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
801 None
802 }
803
804 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
812 None
813 }
814
815 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
830 vec![]
831 }
832
833 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
844 vec![]
845 }
846
847 fn pre_tool_use_hooks_with_config(
852 &self,
853 _config: &serde_json::Value,
854 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
855 self.pre_tool_use_hooks()
856 }
857
858 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
866 vec![]
867 }
868
869 fn post_tool_exec_hooks_with_config(
874 &self,
875 _config: &serde_json::Value,
876 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
877 self.post_tool_exec_hooks()
878 }
879
880 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
889 vec![]
890 }
891
892 fn tool_definition_hooks_with_config(
897 &self,
898 _config: &serde_json::Value,
899 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
900 self.tool_definition_hooks()
901 }
902
903 fn tool_definition_hooks_with_context(
913 &self,
914 _ctx: &SystemPromptContext,
915 config: &serde_json::Value,
916 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
917 self.tool_definition_hooks_with_config(config)
918 }
919
920 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
928 vec![]
929 }
930
931 fn narrate(
945 &self,
946 _tool_def: Option<&ToolDefinition>,
947 tool_call: &ToolCall,
948 phase: crate::tool_narration::ToolNarrationPhase,
949 locale: Option<&str>,
950 ) -> Option<String> {
951 self.tools()
952 .iter()
953 .find(|tool| tool.name() == tool_call.name)
954 .and_then(|tool| tool.narrate(tool_call, phase, locale))
955 }
956
957 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
973 vec![]
974 }
975
976 fn user_hooks_with_config(
982 &self,
983 _config: &serde_json::Value,
984 ) -> Vec<crate::user_hook_types::UserHookSpec> {
985 self.user_hooks()
986 }
987
988 fn risk_level(&self) -> RiskLevel {
996 RiskLevel::Low
997 }
998
999 fn commands(&self) -> Vec<CommandDescriptor> {
1007 vec![]
1008 }
1009
1010 async fn execute_command(
1024 &self,
1025 request: &ExecuteCommandRequest,
1026 _ctx: &CommandExecutionContext,
1027 ) -> crate::error::Result<CommandResult> {
1028 Err(crate::error::AgentLoopError::config(format!(
1029 "capability {} declared command /{} but does not implement execute_command",
1030 self.id(),
1031 request.name,
1032 )))
1033 }
1034
1035 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1044 vec![]
1045 }
1046
1047 fn contribute_skills(&self) -> Vec<SkillContribution> {
1057 vec![]
1058 }
1059
1060 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1071 vec![]
1072 }
1073
1074 fn post_output_guardrails_with_config(
1086 &self,
1087 _config: &serde_json::Value,
1088 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1089 vec![]
1090 }
1091}
1092
1093pub trait ToolDefinitionHook: Send + Sync {
1094 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1095
1096 fn applies_with_native_tool_search(&self) -> bool {
1101 true
1102 }
1103}
1104
1105pub trait ToolCallHook: Send + Sync {
1106 fn narration(
1107 &self,
1108 _tool_def: Option<&ToolDefinition>,
1109 _tool_call: &ToolCall,
1110 _phase: crate::tool_narration::ToolNarrationPhase,
1111 _locale: Option<&str>,
1112 ) -> Option<String> {
1113 None
1114 }
1115
1116 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1117 tool_call
1118 }
1119}
1120
1121pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1127
1128impl ToolCallHook for CapabilityNarrationHook {
1129 fn narration(
1130 &self,
1131 tool_def: Option<&ToolDefinition>,
1132 tool_call: &ToolCall,
1133 phase: crate::tool_narration::ToolNarrationPhase,
1134 locale: Option<&str>,
1135 ) -> Option<String> {
1136 self.0.narrate(tool_def, tool_call, phase, locale)
1137 }
1138}
1139
1140#[derive(
1144 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1145)]
1146#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1147#[cfg_attr(feature = "openapi", schema(example = "low"))]
1148#[serde(rename_all = "lowercase")]
1149pub enum RiskLevel {
1150 Low,
1152 Medium,
1154 High,
1156}
1157
1158#[derive(Debug, Clone, Serialize, Deserialize)]
1164#[serde(rename_all = "snake_case")]
1165pub enum BlueprintModel {
1166 Fixed(String),
1168 Default(String),
1170 Inherit,
1172}
1173
1174pub struct AgentBlueprint {
1180 pub id: &'static str,
1182 pub name: &'static str,
1184 pub description: &'static str,
1186 pub model: BlueprintModel,
1188 pub system_prompt: &'static str,
1190 pub tools: Vec<Box<dyn Tool>>,
1192 pub max_turns: Option<usize>,
1194 pub config_schema: Option<serde_json::Value>,
1196}
1197
1198impl AgentBlueprint {
1199 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1201 self.tools.iter().map(|t| t.to_definition()).collect()
1202 }
1203}
1204
1205impl std::fmt::Debug for AgentBlueprint {
1206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1207 f.debug_struct("AgentBlueprint")
1208 .field("id", &self.id)
1209 .field("name", &self.name)
1210 .field("model", &self.model)
1211 .field("tool_count", &self.tools.len())
1212 .field("max_turns", &self.max_turns)
1213 .finish()
1214 }
1215}
1216
1217#[derive(Clone)]
1244pub struct CapabilityRegistry {
1245 capabilities: HashMap<String, Arc<dyn Capability>>,
1246 aliases: HashMap<String, String>,
1248}
1249
1250impl CapabilityRegistry {
1251 pub fn new() -> Self {
1253 Self {
1254 capabilities: HashMap::new(),
1255 aliases: HashMap::new(),
1256 }
1257 }
1258
1259 pub fn with_builtins() -> Self {
1264 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1265 }
1266
1267 pub fn runtime_builtins() -> Self {
1278 let mut registry = Self::new();
1279
1280 registry.register(AgentInstructionsCapability);
1281 registry.register(HumanIntentCapability);
1282 registry.register(NoopCapability);
1283 registry.register(CurrentTimeCapability);
1284 registry.register(MessageMetadataCapability);
1285 registry.register(FileSystemCapability);
1286 registry.register(SessionStorageCapability);
1287 registry.register(SessionCapability);
1288 registry.register(StatelessTodoListCapability);
1289 #[cfg(feature = "web-fetch")]
1290 registry.register(WebFetchCapability::from_env());
1291 registry.register(BashkitShellCapability);
1292 registry.register(BtwCapability);
1293 registry.register(InfinityContextCapability);
1294 registry.register(budgeting::BudgetingCapability);
1295 registry.register(SelfBudgetCapability);
1296 registry.register(CompactionCapability);
1297 registry.register(ErrorDisclosureCapability);
1298 registry.register(OpenAiToolSearchCapability::new());
1299 registry.register(ClaudeToolSearchCapability::new());
1300 registry.register(ToolSearchCapability::new());
1301 registry.register(AutoToolSearchCapability::new());
1302 registry.register(PromptCachingCapability::new());
1303 registry.register(ParallelToolCallsCapability);
1304 registry.register(SkillsCapability);
1305 registry.register(SystemCommandsCapability);
1306 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1307 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1308 registry.register(LoopDetectionCapability);
1309 registry.register(ToolCallRepairCapability);
1310 registry.register(PromptCanaryGuardrailCapability);
1311 registry.register(GuardrailsCapability);
1312 registry.register(user_hooks::UserHooksCapability);
1313
1314 let internal_flags = crate::InternalFeatureFlags::from_env();
1315 if internal_flags.lua {
1316 registry.register(LuaCapability);
1317 registry.register(LuaCodeModeCapability);
1318 }
1319
1320 registry
1321 }
1322
1323 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1328 let mut registry = Self::new();
1329
1330 registry.register(AgentInstructionsCapability);
1332 registry.register(HumanIntentCapability);
1333 registry.register(NoopCapability);
1334 registry.register(CurrentTimeCapability);
1335 registry.register(MessageMetadataCapability);
1336 registry.register(ResearchCapability);
1337 registry.register(ModelScoutCapability);
1338 registry.register(OpenRouterWorkspaceCapability);
1339 registry.register(OpenRouterServerToolsCapability);
1340 registry.register(PlatformManagementCapability);
1341 registry.register(FileSystemCapability);
1342 registry.register(MemoryCapability);
1343 registry.register(SessionStorageCapability);
1344 registry.register(SessionCapability);
1345 registry.register(SessionSqlDatabaseCapability);
1346 registry.register(TestMathCapability);
1347 registry.register(TestWeatherCapability);
1348 registry.register(StatelessTodoListCapability);
1349 #[cfg(feature = "web-fetch")]
1350 registry.register(WebFetchCapability::from_env());
1351 registry.register(BashkitShellCapability);
1352 registry.register(BackgroundExecutionCapability);
1353 registry.register(SessionScheduleCapability);
1354 registry.register(BtwCapability);
1355 registry.register(InfinityContextCapability);
1356 registry.register(budgeting::BudgetingCapability);
1357 registry.register(SelfBudgetCapability);
1358 registry.register(CompactionCapability);
1359 registry.register(ErrorDisclosureCapability);
1360
1361 registry.register(OpenAiToolSearchCapability::new());
1363 registry.register(ClaudeToolSearchCapability::new());
1365 registry.register(ToolSearchCapability::new());
1367 registry.register(AutoToolSearchCapability::new());
1369 registry.register(PromptCachingCapability::new());
1370
1371 registry.register(ParallelToolCallsCapability);
1373
1374 registry.register(SkillsCapability);
1376
1377 registry.register(SubagentCapability);
1379
1380 registry.register(SessionTasksCapability);
1382
1383 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1387 registry.register(AgentHandoffCapability);
1388 #[cfg(feature = "a2a")]
1392 registry.register(A2aAgentDelegationCapability);
1393 }
1394
1395 registry.register(SystemCommandsCapability);
1397
1398 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1400 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1401
1402 registry.register(user_hooks::UserHooksCapability);
1405
1406 registry.register(LoopDetectionCapability);
1408
1409 registry.register(ToolCallRepairCapability);
1413
1414 registry.register(PromptCanaryGuardrailCapability);
1417
1418 registry.register(GuardrailsCapability);
1421
1422 #[cfg(feature = "ui-capabilities")]
1424 {
1425 registry.register(OpenUiCapability);
1426 registry.register(A2UiCapability);
1427 }
1428
1429 registry.register(SampleDataCapability);
1431
1432 registry.register(DataKnowledgeCapability);
1434
1435 registry.register(KnowledgeBaseCapability);
1437
1438 registry.register(KnowledgeIndexCapability);
1440
1441 registry.register(FakeWarehouseCapability);
1443 registry.register(FakeAwsCapability);
1444 registry.register(FakeCrmCapability);
1445 registry.register(FakeFinancialCapability);
1446
1447 let internal_flags = crate::InternalFeatureFlags::from_env();
1449 if internal_flags.session_sandbox {
1450 registry.register(SessionSandboxCapability);
1451 }
1452
1453 if internal_flags.lua {
1457 registry.register(LuaCapability);
1458 registry.register(LuaCodeModeCapability);
1461 }
1462 for plugin in inventory::iter::<IntegrationPlugin>() {
1463 if (!plugin.experimental_only || grade.experimental_features_enabled())
1464 && plugin
1465 .feature_flag
1466 .is_none_or(|f| internal_flags.is_enabled(f))
1467 {
1468 registry.register_boxed((plugin.factory)());
1469 }
1470 }
1471
1472 registry
1473 }
1474
1475 pub fn register(&mut self, capability: impl Capability + 'static) {
1477 self.register_arc(Arc::new(capability));
1478 }
1479
1480 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1482 self.register_arc(Arc::from(capability));
1483 }
1484
1485 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1487 let canonical = capability.id().to_string();
1488 for alias in capability.aliases() {
1489 self.aliases.insert(alias.to_string(), canonical.clone());
1490 }
1491 self.capabilities.insert(canonical, capability);
1492 }
1493
1494 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1496 self.capabilities
1497 .get(id)
1498 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1499 }
1500
1501 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1506 if self.capabilities.contains_key(id) {
1507 Some(id)
1508 } else {
1509 self.aliases
1510 .get(id)
1511 .filter(|c| self.capabilities.contains_key(*c))
1512 .map(String::as_str)
1513 }
1514 }
1515
1516 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1518 let canonical = self.canonical_id(id)?.to_string();
1519 let removed = self.capabilities.remove(&canonical);
1520 self.aliases.retain(|_, target| *target != canonical);
1521 removed
1522 }
1523
1524 pub fn has(&self, id: &str) -> bool {
1526 self.get(id).is_some()
1527 }
1528
1529 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1531 self.capabilities.values().collect()
1532 }
1533
1534 pub fn len(&self) -> usize {
1536 self.capabilities.len()
1537 }
1538
1539 pub fn is_empty(&self) -> bool {
1541 self.capabilities.is_empty()
1542 }
1543
1544 pub fn builder() -> CapabilityRegistryBuilder {
1546 CapabilityRegistryBuilder::new()
1547 }
1548
1549 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1553 for cap in self.capabilities.values() {
1554 for bp in cap.agent_blueprints() {
1555 if bp.id == id {
1556 return Some(bp);
1557 }
1558 }
1559 }
1560 None
1561 }
1562
1563 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1567 for (capability_id, cap) in &self.capabilities {
1568 for bp in cap.agent_blueprints() {
1569 if bp.id == id {
1570 return Some((capability_id.clone(), bp));
1571 }
1572 }
1573 }
1574 None
1575 }
1576
1577 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1579 self.capabilities
1580 .values()
1581 .flat_map(|cap| cap.agent_blueprints())
1582 .collect()
1583 }
1584}
1585
1586impl Default for CapabilityRegistry {
1587 fn default() -> Self {
1588 Self::with_builtins()
1589 }
1590}
1591
1592impl std::fmt::Debug for CapabilityRegistry {
1593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1594 let ids: Vec<_> = self.capabilities.keys().collect();
1595 f.debug_struct("CapabilityRegistry")
1596 .field("capabilities", &ids)
1597 .finish()
1598 }
1599}
1600
1601pub struct CapabilityRegistryBuilder {
1603 registry: CapabilityRegistry,
1604}
1605
1606impl CapabilityRegistryBuilder {
1607 pub fn new() -> Self {
1609 Self {
1610 registry: CapabilityRegistry::new(),
1611 }
1612 }
1613
1614 pub fn with_builtins() -> Self {
1616 Self {
1617 registry: CapabilityRegistry::with_builtins(),
1618 }
1619 }
1620
1621 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1623 self.registry.register(capability);
1624 self
1625 }
1626
1627 pub fn build(self) -> CapabilityRegistry {
1629 self.registry
1630 }
1631}
1632
1633impl Default for CapabilityRegistryBuilder {
1634 fn default() -> Self {
1635 Self::new()
1636 }
1637}
1638
1639pub struct ModelViewContext<'a> {
1645 pub session_id: SessionId,
1646 pub prior_usage: Option<&'a TokenUsage>,
1647}
1648
1649pub trait ModelViewProvider: Send + Sync {
1655 fn apply_model_view(
1656 &self,
1657 messages: Vec<Message>,
1658 config: &serde_json::Value,
1659 context: &ModelViewContext<'_>,
1660 ) -> Vec<Message>;
1661
1662 fn priority(&self) -> i32 {
1663 0
1664 }
1665}
1666
1667pub struct CollectedCapabilities {
1672 pub system_prompt_parts: Vec<String>,
1674 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1676 pub tools: Vec<Box<dyn Tool>>,
1678 pub tool_definitions: Vec<ToolDefinition>,
1680 pub mounts: Vec<MountPoint>,
1682 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1684 pub applied_ids: Vec<String>,
1686 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1688 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1690 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1693 pub parallel_tool_calls: Option<bool>,
1697 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1699 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1701 pub mcp_servers: ScopedMcpServers,
1703 }
1709
1710#[derive(Debug, Clone, PartialEq, Eq)]
1711pub struct SystemPromptAttribution {
1712 pub capability_id: String,
1713 pub content: String,
1714}
1715
1716impl CollectedCapabilities {
1717 pub fn system_prompt_prefix(&self) -> Option<String> {
1720 if self.system_prompt_parts.is_empty() {
1721 None
1722 } else {
1723 Some(self.system_prompt_parts.join("\n\n"))
1724 }
1725 }
1726
1727 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1731 for (provider, config) in &self.message_filter_providers {
1733 provider.apply_filters(query, config);
1734 }
1735 }
1736
1737 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1740 for (provider, config) in &self.message_filter_providers {
1741 provider.post_load(messages, config);
1742 }
1743 }
1744
1745 pub fn has_message_filters(&self) -> bool {
1747 !self.message_filter_providers.is_empty()
1748 }
1749}
1750
1751struct SpawnAgentTargetProvider {
1752 target_type: &'static str,
1753 tool: Box<dyn Tool>,
1754}
1755
1756struct UnifiedSpawnAgentTool {
1757 providers: Vec<SpawnAgentTargetProvider>,
1758}
1759
1760impl UnifiedSpawnAgentTool {
1761 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1762 Self { providers }
1763 }
1764
1765 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1766 self.providers
1767 .iter()
1768 .find(|provider| provider.target_type == target_type)
1769 .map(|provider| provider.tool.as_ref())
1770 }
1771
1772 fn target_types(&self) -> Vec<&'static str> {
1773 ["subagent", "agent", "external_a2a"]
1774 .into_iter()
1775 .filter(|target_type| {
1776 self.providers
1777 .iter()
1778 .any(|provider| provider.target_type == *target_type)
1779 })
1780 .collect()
1781 }
1782
1783 fn normalize_arguments(&self, mut arguments: serde_json::Value) -> serde_json::Value {
1784 let target_type = arguments
1785 .get("target")
1786 .and_then(|target| target.get("type"))
1787 .and_then(serde_json::Value::as_str)
1788 .unwrap_or_default();
1789
1790 if target_type == "external_a2a" {
1791 if let Some(target) = arguments
1792 .get_mut("target")
1793 .and_then(serde_json::Value::as_object_mut)
1794 && !target.contains_key("external_agent_id")
1795 && let Some(id) = target.get("id").cloned()
1796 {
1797 target.insert("external_agent_id".to_string(), id);
1798 }
1799 if arguments.get("mode").and_then(serde_json::Value::as_str) == Some("foreground") {
1800 arguments["mode"] = serde_json::Value::String("wait".to_string());
1801 }
1802 } else if matches!(target_type, "subagent" | "agent")
1803 && arguments.get("mode").and_then(serde_json::Value::as_str) == Some("wait")
1804 {
1805 arguments["mode"] = serde_json::Value::String("foreground".to_string());
1806 }
1807
1808 arguments
1809 }
1810}
1811
1812#[async_trait]
1813impl Tool for UnifiedSpawnAgentTool {
1814 fn narrate(
1815 &self,
1816 tool_call: &ToolCall,
1817 phase: crate::tool_narration::ToolNarrationPhase,
1818 locale: Option<&str>,
1819 ) -> Option<String> {
1820 let target_type = tool_call
1821 .arguments
1822 .get("target")
1823 .and_then(|target| target.get("type"))
1824 .and_then(serde_json::Value::as_str)?;
1825 self.provider_for(target_type)
1826 .and_then(|tool| tool.narrate(tool_call, phase, locale))
1827 }
1828
1829 fn name(&self) -> &str {
1830 "spawn_agent"
1831 }
1832
1833 fn display_name(&self) -> Option<&str> {
1834 Some("Spawn Agent")
1835 }
1836
1837 fn description(&self) -> &str {
1838 "Delegate work to another agent target. Set target.type to one of the advertised target types; background returns a task_id for generic task tools, and foreground waits for the result."
1839 }
1840
1841 fn parameters_schema(&self) -> serde_json::Value {
1842 serde_json::json!({
1843 "type": "object",
1844 "properties": {
1845 "name": {
1846 "type": "string",
1847 "description": "Human-readable name for local subagent or first-party handoff runs. Optional for external A2A targets."
1848 },
1849 "instructions": {
1850 "type": "string",
1851 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
1852 },
1853 "goal": {
1854 "type": "string",
1855 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
1856 },
1857 "lifetime": {
1858 "type": "string",
1859 "enum": ["linked", "detached"],
1860 "default": "linked",
1861 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
1862 },
1863 "seed": {
1864 "type": "string",
1865 "enum": ["fresh", "fork", "workspace"],
1866 "default": "fresh",
1867 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
1868 },
1869 "target": {
1870 "type": "object",
1871 "properties": {
1872 "type": {
1873 "type": "string",
1874 "enum": self.target_types(),
1875 "description": "Delegation target type. Use subagent for same-agent child sessions, agent for configured first-party handoffs, or external_a2a for configured remote A2A agents."
1876 },
1877 "id": {
1878 "type": "string",
1879 "description": "Configured first-party handoff target id. Also accepted as an alias for external_a2a target.external_agent_id."
1880 },
1881 "external_agent_id": {
1882 "type": "string",
1883 "description": "Configured external A2A agent id."
1884 }
1885 },
1886 "required": ["type"],
1887 "additionalProperties": false
1888 },
1889 "mode": {
1890 "type": "string",
1891 "enum": ["background", "foreground", "wait"],
1892 "description": "Execution mode. Use background to return immediately with a task_id. Use foreground to block for local targets; external_a2a also accepts legacy wait."
1893 },
1894 "blueprint": {
1895 "type": "string",
1896 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
1897 },
1898 "config": {
1899 "type": "object",
1900 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
1901 },
1902 "result_schema": {
1903 "type": "object",
1904 "description": "Subagent-only JSON Schema for the child agent's final structured result. When set, the child must call report_result before the task can succeed."
1905 },
1906 "message_schema": {
1907 "type": "object",
1908 "description": "Subagent-only JSON Schema for structured progress messages. When set, the child receives report_task_progress and valid calls post data messages to the task thread."
1909 },
1910 "public_context": {
1911 "type": "object",
1912 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
1913 },
1914 "wait_timeout_secs": {
1915 "type": "integer",
1916 "minimum": 1,
1917 "maximum": 86400,
1918 "description": "External-A2A-only foreground/wait timeout."
1919 },
1920 "wake_on_completion": {
1921 "type": "boolean",
1922 "description": "External-A2A-only control for background completion wake-ups."
1923 }
1924 },
1925 "required": ["instructions", "target"],
1926 "additionalProperties": false
1927 })
1928 }
1929
1930 fn hints(&self) -> crate::tool_types::ToolHints {
1931 let mut hints = crate::tool_types::ToolHints::default().with_long_running(true);
1932 if self.provider_for("external_a2a").is_some() {
1933 hints = hints.with_open_world(true);
1934 }
1935 hints
1936 }
1937
1938 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
1939 ToolExecutionResult::tool_error(
1940 "spawn_agent requires context. This tool must be executed with session context.",
1941 )
1942 }
1943
1944 async fn execute_with_context(
1945 &self,
1946 arguments: serde_json::Value,
1947 context: &ToolContext,
1948 ) -> ToolExecutionResult {
1949 let target_type = match arguments
1950 .get("target")
1951 .and_then(|target| target.get("type"))
1952 .and_then(serde_json::Value::as_str)
1953 {
1954 Some(target_type) => target_type,
1955 None => {
1956 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
1957 }
1958 };
1959
1960 let Some(provider) = self.provider_for(target_type) else {
1961 let supported = self.target_types().join(", ");
1962 return ToolExecutionResult::tool_error(format!(
1963 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
1964 ));
1965 };
1966 if target_type == "external_a2a"
1967 && arguments
1968 .get("lifetime")
1969 .and_then(serde_json::Value::as_str)
1970 .is_some_and(|value| value == "detached")
1971 {
1972 return ToolExecutionResult::tool_error(
1973 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
1974 );
1975 }
1976
1977 provider
1978 .execute_with_context(self.normalize_arguments(arguments), context)
1979 .await
1980 }
1981
1982 fn requires_context(&self) -> bool {
1983 true
1984 }
1985}
1986
1987pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1992 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1993 return base_system_prompt.to_string();
1994 };
1995
1996 if base_system_prompt.is_empty() {
1997 return additions.to_string();
1998 }
1999
2000 if base_system_prompt.contains("<system-prompt>") {
2001 format!("{base_system_prompt}\n\n{additions}")
2002 } else {
2003 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2004 }
2005}
2006
2007pub struct CollectedMessageFilters {
2014 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2016}
2017
2018pub struct CollectedModelViewProviders {
2020 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2022}
2023
2024impl CollectedMessageFilters {
2030 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2032 for (provider, config) in &self.message_filter_providers {
2033 provider.apply_filters(query, config);
2034 }
2035 }
2036
2037 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2039 for (provider, config) in &self.message_filter_providers {
2040 provider.post_load(messages, config);
2041 }
2042 }
2043}
2044
2045impl CollectedModelViewProviders {
2046 pub fn apply_model_view(
2048 &self,
2049 mut messages: Vec<Message>,
2050 context: &ModelViewContext<'_>,
2051 ) -> Vec<Message> {
2052 for (provider, config) in &self.model_view_providers {
2053 messages = provider.apply_model_view(messages, config, context);
2054 }
2055 messages
2056 }
2057}
2058
2059fn compaction_is_enabled(
2065 capability_configs: &[AgentCapabilityConfig],
2066 registry: &CapabilityRegistry,
2067) -> bool {
2068 capability_configs.iter().any(|cap_config| {
2069 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2070 && registry
2071 .get(cap_config.capability_ref.as_str())
2072 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2073 })
2074}
2075
2076fn message_filter_config_for(
2085 cap_id: &str,
2086 base: &serde_json::Value,
2087 compaction_on: bool,
2088) -> serde_json::Value {
2089 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2090 return base.clone();
2091 }
2092 let mut config = base.clone();
2093 match config.as_object_mut() {
2094 Some(map) => {
2095 map.insert(
2096 "compaction_active".to_string(),
2097 serde_json::Value::Bool(true),
2098 );
2099 }
2100 None => {
2101 config = serde_json::json!({ "compaction_active": true });
2102 }
2103 }
2104 config
2105}
2106
2107pub fn collect_message_filters_only(
2113 capability_configs: &[AgentCapabilityConfig],
2114 registry: &CapabilityRegistry,
2115) -> CollectedMessageFilters {
2116 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2117 Vec::new();
2118 let compaction_on = compaction_is_enabled(capability_configs, registry);
2119
2120 for cap_config in capability_configs {
2121 let cap_id = cap_config.capability_ref.as_str();
2122 if let Some(capability) = registry.get(cap_id) {
2123 if capability.status() != CapabilityStatus::Available {
2124 continue;
2125 }
2126 let effective: &dyn Capability = capability
2129 .resolve_for_model(None)
2130 .unwrap_or_else(|| capability.as_ref());
2131 if let Some(provider) = effective.message_filter_provider() {
2132 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2133 message_filter_providers.push((provider, config));
2134 }
2135 }
2136 }
2137
2138 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2139
2140 CollectedMessageFilters {
2141 message_filter_providers,
2142 }
2143}
2144
2145pub fn collect_model_view_providers(
2152 capability_configs: &[AgentCapabilityConfig],
2153 registry: &CapabilityRegistry,
2154 model: Option<&str>,
2155) -> CollectedModelViewProviders {
2156 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2157
2158 for cap_config in capability_configs {
2159 let cap_id = cap_config.capability_ref.as_str();
2160 if let Some(capability) = registry.get(cap_id) {
2161 if capability.status() != CapabilityStatus::Available {
2162 continue;
2163 }
2164 let effective: &dyn Capability = capability
2165 .resolve_for_model(model)
2166 .unwrap_or_else(|| capability.as_ref());
2167 if let Some(provider) = effective.model_view_provider() {
2168 model_view_providers.push((provider, cap_config.config.clone()));
2169 }
2170 }
2171 }
2172
2173 model_view_providers.sort_by_key(|(p, _)| p.priority());
2174
2175 CollectedModelViewProviders {
2176 model_view_providers,
2177 }
2178}
2179
2180pub fn collect_dynamic_facts(
2186 capability_configs: &[AgentCapabilityConfig],
2187 registry: &CapabilityRegistry,
2188 model: Option<&str>,
2189 ctx: &FactsContext,
2190) -> Vec<Fact> {
2191 let mut dynamic = Vec::new();
2192 for cap_config in capability_configs {
2193 let cap_id = cap_config.capability_ref.as_str();
2194 if let Some(capability) = registry.get(cap_id) {
2195 if capability.status() != CapabilityStatus::Available {
2196 continue;
2197 }
2198 let effective: &dyn Capability = capability
2199 .resolve_for_model(model)
2200 .unwrap_or_else(|| capability.as_ref());
2201 for fact in effective.facts(&cap_config.config, ctx) {
2202 if fact.volatility == Volatility::Dynamic {
2203 dynamic.push(fact);
2204 }
2205 }
2206 }
2207 }
2208 dynamic
2209}
2210
2211pub fn collect_capability_mcp_servers(
2212 capability_configs: &[AgentCapabilityConfig],
2213 registry: &CapabilityRegistry,
2214) -> ScopedMcpServers {
2215 let mut servers = ScopedMcpServers::default();
2216
2217 for cap_config in capability_configs {
2218 let cap_id = cap_config.capability_ref.as_str();
2219 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2222 if let Ok(definition) =
2223 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2224 {
2225 if definition.status != CapabilityStatus::Available {
2226 continue;
2227 }
2228 if let Some(contributed) = definition.mcp_servers {
2229 servers = merge_scoped_mcp_servers(&servers, &contributed);
2230 }
2231 }
2232 continue;
2233 }
2234 if let Some(capability) = registry.get(cap_id) {
2235 if capability.status() != CapabilityStatus::Available {
2236 continue;
2237 }
2238 servers = merge_scoped_mcp_servers(
2239 &servers,
2240 &capability.mcp_servers_with_config(&cap_config.config),
2241 );
2242 }
2243 }
2244
2245 servers
2246}
2247
2248pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2255
2256#[derive(Debug, Clone, PartialEq, Eq)]
2258pub enum DependencyError {
2259 CircularDependency {
2261 capability_id: String,
2263 chain: Vec<String>,
2265 },
2266 TooManyCapabilities {
2268 count: usize,
2270 max: usize,
2272 },
2273}
2274
2275impl std::fmt::Display for DependencyError {
2276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2277 match self {
2278 DependencyError::CircularDependency {
2279 capability_id,
2280 chain,
2281 } => {
2282 write!(
2283 f,
2284 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2285 capability_id,
2286 chain.join(" -> "),
2287 capability_id
2288 )
2289 }
2290 DependencyError::TooManyCapabilities { count, max } => {
2291 write!(
2292 f,
2293 "Too many capabilities after resolution: {} (max: {})",
2294 count, max
2295 )
2296 }
2297 }
2298 }
2299}
2300
2301impl std::error::Error for DependencyError {}
2302
2303#[derive(Debug, Clone)]
2305pub struct ResolvedCapabilities {
2306 pub resolved_ids: Vec<String>,
2309 pub added_as_dependencies: Vec<String>,
2311 pub user_selected: Vec<String>,
2313}
2314
2315pub fn resolve_dependencies(
2335 selected_ids: &[String],
2336 registry: &CapabilityRegistry,
2337) -> Result<ResolvedCapabilities, DependencyError> {
2338 use std::collections::HashSet;
2339
2340 let user_selected: HashSet<String> = selected_ids
2342 .iter()
2343 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2344 .collect();
2345 let mut resolved: Vec<String> = Vec::new();
2346 let mut resolved_set: HashSet<String> = HashSet::new();
2347 let mut added_as_dependencies: Vec<String> = Vec::new();
2348
2349 for cap_id in selected_ids {
2351 resolve_single_capability(
2352 cap_id,
2353 registry,
2354 &mut resolved,
2355 &mut resolved_set,
2356 &mut added_as_dependencies,
2357 &user_selected,
2358 &mut Vec::new(), )?;
2360 }
2361
2362 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2364 return Err(DependencyError::TooManyCapabilities {
2365 count: resolved.len(),
2366 max: MAX_RESOLVED_CAPABILITIES,
2367 });
2368 }
2369
2370 Ok(ResolvedCapabilities {
2371 resolved_ids: resolved,
2372 added_as_dependencies,
2373 user_selected: selected_ids.to_vec(),
2374 })
2375}
2376
2377pub fn resolve_capability_configs(
2382 selected_configs: &[AgentCapabilityConfig],
2383 registry: &CapabilityRegistry,
2384) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2385 let mut selected_ids: Vec<String> = Vec::new();
2386 for config in selected_configs {
2387 if (is_declarative_capability(config.capability_id())
2390 || is_plugin_capability(config.capability_id()))
2391 && let Ok(definition) =
2392 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2393 {
2394 selected_ids.extend(definition.dependencies);
2395 }
2396 selected_ids.push(config.capability_id().to_string());
2397 }
2398 let resolved = resolve_dependencies(&selected_ids, registry)?;
2399
2400 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2403 .iter()
2404 .map(|config| {
2405 let id = config.capability_id();
2406 let id = registry.canonical_id(id).unwrap_or(id);
2407 (id.to_string(), config.config.clone())
2408 })
2409 .collect();
2410
2411 Ok(resolved
2412 .resolved_ids
2413 .into_iter()
2414 .map(|capability_id| {
2415 explicit_configs
2416 .get(&capability_id)
2417 .cloned()
2418 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2419 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2420 })
2421 .collect())
2422}
2423
2424fn resolve_single_capability(
2426 cap_id: &str,
2427 registry: &CapabilityRegistry,
2428 resolved: &mut Vec<String>,
2429 resolved_set: &mut std::collections::HashSet<String>,
2430 added_as_dependencies: &mut Vec<String>,
2431 user_selected: &std::collections::HashSet<String>,
2432 visiting: &mut Vec<String>,
2433) -> Result<(), DependencyError> {
2434 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2438
2439 if resolved_set.contains(cap_id) {
2441 return Ok(());
2442 }
2443
2444 if visiting.contains(&cap_id.to_string()) {
2446 return Err(DependencyError::CircularDependency {
2447 capability_id: cap_id.to_string(),
2448 chain: visiting.clone(),
2449 });
2450 }
2451
2452 let capability = match registry.get(cap_id) {
2454 Some(cap) => cap,
2455 None => {
2456 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2460 && !resolved_set.contains(cap_id)
2461 {
2462 resolved.push(cap_id.to_string());
2463 resolved_set.insert(cap_id.to_string());
2464 if !user_selected.contains(cap_id) {
2465 added_as_dependencies.push(cap_id.to_string());
2466 }
2467 }
2468 return Ok(());
2469 }
2470 };
2471
2472 visiting.push(cap_id.to_string());
2474
2475 for dep_id in capability.dependencies() {
2477 resolve_single_capability(
2478 dep_id,
2479 registry,
2480 resolved,
2481 resolved_set,
2482 added_as_dependencies,
2483 user_selected,
2484 visiting,
2485 )?;
2486 }
2487
2488 visiting.pop();
2490
2491 if !resolved_set.contains(cap_id) {
2493 resolved.push(cap_id.to_string());
2494 resolved_set.insert(cap_id.to_string());
2495
2496 if !user_selected.contains(cap_id) {
2498 added_as_dependencies.push(cap_id.to_string());
2499 }
2500 }
2501
2502 Ok(())
2503}
2504
2505pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2510 use std::collections::HashSet;
2511
2512 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2513 Ok(resolved) => resolved.resolved_ids,
2514 Err(_) => capability_ids.to_vec(),
2515 };
2516
2517 let mut seen = HashSet::new();
2518 let mut features = Vec::new();
2519 for cap_id in &resolved_ids {
2520 if let Some(cap) = registry.get(cap_id) {
2521 for feature in cap.features() {
2522 if seen.insert(feature) {
2523 features.push(feature.to_string());
2524 }
2525 }
2526 }
2527 }
2528 features
2529}
2530
2531pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2534 registry
2535 .get(cap_id)
2536 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2537 .unwrap_or_default()
2538}
2539
2540pub async fn collect_capabilities(
2556 capability_ids: &[String],
2557 registry: &CapabilityRegistry,
2558 ctx: &SystemPromptContext,
2559) -> CollectedCapabilities {
2560 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2563 Ok(resolved) => resolved.resolved_ids,
2564 Err(e) => {
2565 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2566 capability_ids.to_vec()
2567 }
2568 };
2569
2570 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2572 .iter()
2573 .map(|id| AgentCapabilityConfig {
2574 capability_ref: CapabilityId::new(id),
2575 config: serde_json::Value::Object(serde_json::Map::new()),
2576 })
2577 .collect();
2578
2579 collect_capabilities_with_configs(&configs, registry, ctx).await
2580}
2581
2582pub async fn collect_capabilities_with_configs(
2593 capability_configs: &[AgentCapabilityConfig],
2594 registry: &CapabilityRegistry,
2595 ctx: &SystemPromptContext,
2596) -> CollectedCapabilities {
2597 let mut system_prompt_parts: Vec<String> = Vec::new();
2598 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2599 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2600 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2601 let mut mounts: Vec<MountPoint> = Vec::new();
2602 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2603 Vec::new();
2604 let mut applied_ids: Vec<String> = Vec::new();
2605 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2606 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2607 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2608 let mut parallel_tool_calls: Option<bool> = None;
2609 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2610 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2611 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2614 let mut mcp_servers = ScopedMcpServers::default();
2615 let mut static_facts: Vec<Fact> = Vec::new();
2619 let mut has_dynamic_facts = false;
2620 let facts_ctx = FactsContext::new(ctx.session_id);
2621 let compaction_on = compaction_is_enabled(capability_configs, registry);
2622 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2623 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2624
2625 for cap_config in capability_configs {
2626 let cap_id = cap_config.capability_ref.as_str();
2627 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2632 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2633 cap_config.config.clone(),
2634 ) {
2635 Ok(definition) => {
2636 if definition.status != CapabilityStatus::Available {
2637 continue;
2638 }
2639
2640 if let Some(prompt) = definition.system_prompt.as_deref() {
2641 let contribution =
2642 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2643 system_prompt_attributions.push(SystemPromptAttribution {
2644 capability_id: cap_id.to_string(),
2645 content: contribution.clone(),
2646 });
2647 system_prompt_parts.push(contribution);
2648 }
2649
2650 mounts.extend(definition.mounts(cap_id));
2651 if let Some(ref servers) = definition.mcp_servers {
2652 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2653 }
2654 for skill in definition.skill_contributions() {
2655 mounts.push(skill.to_mount(cap_id));
2656 }
2657
2658 applied_ids.push(cap_id.to_string());
2659 }
2660 Err(error) => {
2661 tracing::warn!(
2662 capability_id = %cap_id,
2663 error = %error,
2664 "Skipping invalid declarative/plugin capability config"
2665 );
2666 }
2667 }
2668 continue;
2669 }
2670 if let Some(capability) = registry.get(cap_id) {
2671 if capability.status() != CapabilityStatus::Available {
2673 continue;
2674 }
2675
2676 let effective: &dyn Capability =
2688 match capability.resolve_for_model(ctx.model.as_deref()) {
2689 Some(inner) => inner,
2690 None => capability.as_ref(),
2691 };
2692 let effective_id = effective.id();
2693 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2694 agent_handoff_spawn_config = Some(cap_config.config.clone());
2695 }
2696
2697 if let Some(contribution) = effective
2699 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2700 .await
2701 {
2702 system_prompt_attributions.push(SystemPromptAttribution {
2703 capability_id: cap_id.to_string(),
2704 content: contribution.clone(),
2705 });
2706 system_prompt_parts.push(contribution);
2707 }
2708
2709 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2714 match fact.volatility {
2715 Volatility::Static => static_facts.push(fact),
2716 Volatility::Dynamic => has_dynamic_facts = true,
2717 }
2718 }
2719
2720 for tool in effective.tools_with_config(&cap_config.config) {
2722 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2723 spawn_agent_providers.push(SpawnAgentTargetProvider {
2724 target_type: "external_a2a",
2725 tool,
2726 });
2727 } else {
2728 tools.push(tool);
2729 }
2730 }
2731 tool_definition_hooks
2732 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2733 tool_call_hooks.extend(effective.tool_call_hooks());
2734 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2736 let cap_category = effective.category();
2741 for def in effective.tool_definitions() {
2742 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2743 continue;
2744 }
2745 let def = match (def.category(), cap_category) {
2746 (None, Some(cat)) => def.with_category(cat),
2747 _ => def,
2748 }
2749 .with_capability_attribution(cap_id, Some(capability.name()));
2750 tool_definitions.push(def);
2751 }
2752
2753 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2761 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2762 {
2763 let threshold = cap_config
2765 .config
2766 .get("threshold")
2767 .and_then(|v| v.as_u64())
2768 .map(|v| v as usize)
2769 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2770 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2771 enabled: true,
2772 threshold,
2773 });
2774 }
2775
2776 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2777 let strategy = cap_config
2778 .config
2779 .get("strategy")
2780 .and_then(|v| v.as_str())
2781 .map(|value| match value {
2782 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2783 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2784 })
2785 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2786 let gemini_cached_content = cap_config
2787 .config
2788 .get("gemini_cached_content")
2789 .and_then(|v| v.as_str())
2790 .map(str::to_string);
2791 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2792 enabled: true,
2793 strategy,
2794 gemini_cached_content,
2795 });
2796 }
2797
2798 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2799 parallel_tool_calls =
2800 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2801 }
2802
2803 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2804 let server_tools =
2805 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2806 if !server_tools.is_empty() {
2807 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2808 server_tools,
2809 ..Default::default()
2810 });
2811 }
2812 }
2813
2814 mounts.extend(effective.mounts());
2816
2817 mcp_servers = merge_scoped_mcp_servers(
2818 &mcp_servers,
2819 &effective.mcp_servers_with_config(&cap_config.config),
2820 );
2821
2822 for skill in effective.contribute_skills() {
2826 mounts.push(skill.to_mount(cap_id));
2827 }
2828
2829 if let Some(provider) = effective.message_filter_provider() {
2831 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2832 message_filter_providers.push((provider, config));
2833 }
2834
2835 applied_ids.push(cap_id.to_string());
2836 }
2837 }
2838
2839 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
2844 spawn_agent_providers.push(SpawnAgentTargetProvider {
2845 target_type: "subagent",
2846 tool: Box::new(SpawnSubagentAsAgentTool),
2847 });
2848 }
2849 if let Some(config) = agent_handoff_spawn_config.as_ref() {
2850 spawn_agent_providers.push(SpawnAgentTargetProvider {
2851 target_type: "agent",
2852 tool: Box::new(SpawnAgentHandoffTool::new(config)),
2853 });
2854 }
2855 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
2856 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
2857 let def = tool
2858 .to_definition()
2859 .with_category("Orchestration")
2860 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
2861 tools.push(Box::new(tool));
2862 tool_definitions.push(def);
2863 }
2864
2865 if !applied_ids
2877 .iter()
2878 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2879 && tool_definitions
2880 .iter()
2881 .any(|def| def.hints().supports_background == Some(true))
2882 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2883 && bg_cap.status() == CapabilityStatus::Available
2884 {
2885 tools.extend(bg_cap.tools());
2886 let cap_category = bg_cap.category();
2887 for def in bg_cap.tool_definitions() {
2888 let def = match (def.category(), cap_category) {
2889 (None, Some(cat)) => def.with_category(cat),
2890 _ => def,
2891 }
2892 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2893 tool_definitions.push(def);
2894 }
2895 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2896 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2897 }
2898
2899 if let Some(block) = facts::render_facts_block(&static_facts) {
2904 system_prompt_attributions.push(SystemPromptAttribution {
2905 capability_id: "facts".to_string(),
2906 content: block.clone(),
2907 });
2908 system_prompt_parts.push(block);
2909 }
2910 if has_dynamic_facts {
2911 system_prompt_attributions.push(SystemPromptAttribution {
2912 capability_id: "facts".to_string(),
2913 content: FACTS_DYNAMIC_NOTE.to_string(),
2914 });
2915 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
2916 }
2917
2918 tool_call_hooks.extend(narration_hooks);
2922
2923 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2925
2926 CollectedCapabilities {
2927 system_prompt_parts,
2928 system_prompt_attributions,
2929 tools,
2930 tool_definitions,
2931 mounts,
2932 message_filter_providers,
2933 applied_ids,
2934 tool_search,
2935 prompt_cache,
2936 openrouter_routing,
2937 parallel_tool_calls,
2938 tool_definition_hooks,
2939 tool_call_hooks,
2940 mcp_servers,
2941 }
2942}
2943
2944pub struct AppliedCapabilities {
2950 pub runtime_agent: RuntimeAgent,
2952 pub tool_registry: ToolRegistry,
2954 pub applied_ids: Vec<String>,
2956}
2957
2958pub async fn apply_capabilities(
2995 base_runtime_agent: RuntimeAgent,
2996 capability_ids: &[String],
2997 registry: &CapabilityRegistry,
2998 ctx: &SystemPromptContext,
2999) -> AppliedCapabilities {
3000 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3001
3002 let final_system_prompt = compose_system_prompt(
3004 &base_runtime_agent.system_prompt,
3005 collected.system_prompt_prefix().as_deref(),
3006 );
3007
3008 let mut tool_registry = ToolRegistry::new();
3010 for tool in collected.tools {
3011 tool_registry.register_boxed(tool);
3012 }
3013
3014 let mut tools = collected.tool_definitions;
3016 for hook in &collected.tool_definition_hooks {
3017 tools = hook.transform(tools);
3018 }
3019
3020 let runtime_agent = RuntimeAgent {
3021 system_prompt: final_system_prompt,
3022 model: base_runtime_agent.model,
3023 tools,
3024 max_iterations: base_runtime_agent.max_iterations,
3025 temperature: base_runtime_agent.temperature,
3026 max_tokens: base_runtime_agent.max_tokens,
3027 tool_search: collected.tool_search,
3028 prompt_cache: collected.prompt_cache,
3029 openrouter_routing: collected.openrouter_routing,
3030 network_access: base_runtime_agent.network_access,
3031 parallel_tool_calls: base_runtime_agent
3034 .parallel_tool_calls
3035 .or(collected.parallel_tool_calls),
3036 };
3037
3038 AppliedCapabilities {
3039 runtime_agent,
3040 tool_registry,
3041 applied_ids: collected.applied_ids,
3042 }
3043}
3044
3045#[cfg(test)]
3050mod tests {
3051 use super::*;
3052 use crate::typed_id::SessionId;
3053 use std::collections::BTreeSet;
3054 use uuid::Uuid;
3055
3056 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3058
3059 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3060 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3061 }
3062
3063 fn test_ctx() -> SystemPromptContext {
3065 SystemPromptContext::without_file_store(SessionId::new())
3066 }
3067
3068 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3070 let mut ids = [
3071 "agent_instructions",
3072 "human_intent",
3073 "budgeting",
3074 "self_budget",
3075 "noop",
3076 "current_time",
3077 "research",
3078 "platform_management",
3079 "session_file_system",
3080 "session_storage",
3081 "session",
3082 "session_sql_database",
3083 "test_math",
3084 "test_weather",
3085 "stateless_todo_list",
3086 "web_fetch",
3087 "bashkit_shell",
3088 "background_execution",
3089 "session_schedule",
3090 "btw",
3091 "infinity_context",
3092 "compaction",
3093 "memory",
3094 "message_metadata",
3095 "openai_tool_search",
3096 "claude_tool_search",
3097 "tool_search",
3098 "auto_tool_search",
3099 "prompt_caching",
3100 "parallel_tool_calls",
3101 "session_tasks",
3102 "skills",
3103 "subagents",
3104 "system_commands",
3105 "sample_data",
3106 "data_knowledge",
3107 "knowledge_base",
3108 "knowledge_index",
3109 "tool_output_persistence",
3110 "tool_output_distillation",
3111 "fake_warehouse",
3112 "fake_aws",
3113 "fake_crm",
3114 "fake_financial",
3115 "loop_detection",
3116 "tool_call_repair",
3117 "error_disclosure",
3118 "prompt_canary_guardrail",
3119 "guardrails",
3120 "user_hooks",
3121 "model_scout",
3122 "openrouter_workspace",
3123 "openrouter_server_tools",
3124 ]
3125 .into_iter()
3126 .collect::<BTreeSet<_>>();
3127 if cfg!(feature = "ui-capabilities") {
3128 ids.insert("openui");
3129 ids.insert("a2ui");
3130 }
3131 ids
3132 }
3133
3134 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3136 let mut ids = [
3137 "agent_instructions",
3138 "human_intent",
3139 "budgeting",
3140 "self_budget",
3141 "noop",
3142 "current_time",
3143 "session_file_system",
3144 "session_storage",
3145 "session",
3146 "stateless_todo_list",
3147 "bashkit_shell",
3148 "btw",
3149 "infinity_context",
3150 "compaction",
3151 "message_metadata",
3152 "openai_tool_search",
3153 "claude_tool_search",
3154 "tool_search",
3155 "auto_tool_search",
3156 "prompt_caching",
3157 "parallel_tool_calls",
3158 "skills",
3159 "system_commands",
3160 "tool_output_persistence",
3161 "tool_output_distillation",
3162 "loop_detection",
3163 "tool_call_repair",
3164 "error_disclosure",
3165 "prompt_canary_guardrail",
3166 "guardrails",
3167 "user_hooks",
3168 ]
3169 .into_iter()
3170 .collect::<BTreeSet<_>>();
3171 if cfg!(feature = "web-fetch") {
3172 ids.insert("web_fetch");
3173 }
3174 ids
3175 }
3176
3177 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3179 let mut ids = expected_core_builtin_ids();
3180 ids.insert("agent_handoff");
3181 ids.insert("a2a_agent_delegation");
3182 ids
3183 }
3184
3185 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3186 registry.capabilities.keys().map(String::as_str).collect()
3187 }
3188
3189 #[test]
3199 fn test_capability_registry_with_builtins_dev() {
3200 let _lock = lock_env();
3202 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3203 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3204 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3205 assert!(registry.has("agent_handoff"));
3206 assert!(registry.has("a2a_agent_delegation"));
3207 }
3208
3209 #[test]
3210 fn test_capability_registry_with_builtins_prod() {
3211 let _lock = lock_env();
3213 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3214 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3215 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3216 assert!(!registry.has("docker_container"));
3218 assert!(!registry.has("agent_handoff"));
3219 assert!(!registry.has("a2a_agent_delegation"));
3220 }
3221
3222 #[test]
3223 fn test_capability_registry_runtime_builtins() {
3224 let _lock = lock_env();
3225 unsafe { std::env::remove_var("FEATURE_LUA") };
3226 let registry = CapabilityRegistry::runtime_builtins();
3227 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3228 assert!(registry.has("session_file_system"));
3229 #[cfg(feature = "web-fetch")]
3230 assert!(registry.has("web_fetch"));
3231 assert!(registry.has("bashkit_shell"));
3232
3233 for platform_only in [
3234 "platform_management",
3235 "model_scout",
3236 "openrouter_workspace",
3237 "openrouter_server_tools",
3238 "session_tasks",
3239 "session_schedule",
3240 "subagents",
3241 "background_execution",
3242 "session_sql_database",
3243 "knowledge_base",
3244 "knowledge_index",
3245 "sample_data",
3246 "data_knowledge",
3247 "fake_aws",
3248 "fake_crm",
3249 "fake_financial",
3250 "fake_warehouse",
3251 "test_math",
3252 "test_weather",
3253 "research",
3254 ] {
3255 assert!(
3256 !registry.has(platform_only),
3257 "`{platform_only}` should not be in the runtime default registry"
3258 );
3259 }
3260 }
3261
3262 #[test]
3263 fn test_agent_delegation_enabled_by_env_in_prod() {
3264 let _lock = lock_env();
3266 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3267 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3268 assert!(registry.has("agent_handoff"));
3269 assert!(registry.has("a2a_agent_delegation"));
3270 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3271 }
3272
3273 #[test]
3274 fn test_agent_delegation_disabled_by_env_in_dev() {
3275 let _lock = lock_env();
3277 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3278 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3279 assert!(!registry.has("agent_handoff"));
3280 assert!(!registry.has("a2a_agent_delegation"));
3281 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3282 }
3283
3284 #[test]
3285 fn test_capability_registry_get() {
3286 let registry = CapabilityRegistry::with_builtins();
3287
3288 let noop = registry.get("noop").unwrap();
3289 assert_eq!(noop.id(), "noop");
3290 assert_eq!(noop.name(), "No-Op");
3291 assert_eq!(noop.status(), CapabilityStatus::Available);
3292 }
3293
3294 #[test]
3302 fn builtin_capabilities_satisfy_registry_invariants() {
3303 let registry = CapabilityRegistry::with_builtins();
3304
3305 for cap in registry.list() {
3306 let id = cap.id();
3307 assert!(!id.is_empty(), "capability has an empty id");
3308 assert!(
3309 !cap.name().trim().is_empty(),
3310 "capability `{id}` has an empty name"
3311 );
3312
3313 assert!(
3316 registry.get(id).is_some(),
3317 "capability `{id}` does not resolve by its own id"
3318 );
3319
3320 for dep in cap.dependencies() {
3324 assert!(
3325 registry.get(dep).is_some(),
3326 "capability `{id}` depends on `{dep}`, which is not registered"
3327 );
3328 }
3329
3330 let mut seen = std::collections::HashSet::new();
3333 for tool in cap.tools() {
3334 let name = tool.name().to_string();
3335 assert!(
3336 !name.is_empty(),
3337 "capability `{id}` exposes a tool with an empty name"
3338 );
3339 assert!(
3340 seen.insert(name.clone()),
3341 "capability `{id}` exposes duplicate tool name `{name}`"
3342 );
3343 }
3344
3345 let mut def_seen = std::collections::HashSet::new();
3348 for def in cap.tool_definitions() {
3349 let name = def.name().to_string();
3350 assert!(
3351 !name.is_empty(),
3352 "capability `{id}` advertises a tool definition with an empty name"
3353 );
3354 assert!(
3355 def_seen.insert(name.clone()),
3356 "capability `{id}` advertises duplicate tool definition name `{name}`"
3357 );
3358 }
3359 }
3360 }
3361
3362 #[test]
3363 fn test_capability_registry_blueprint_with_capability() {
3364 struct BlueprintProviderCapability;
3365
3366 impl Capability for BlueprintProviderCapability {
3367 fn id(&self) -> &str {
3368 "blueprint_provider"
3369 }
3370 fn name(&self) -> &str {
3371 "Blueprint Provider"
3372 }
3373 fn description(&self) -> &str {
3374 "Capability that provides a blueprint for tests"
3375 }
3376 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3377 vec![AgentBlueprint {
3378 id: "test_blueprint",
3379 name: "Test Blueprint",
3380 description: "Blueprint for capability registry tests",
3381 model: BlueprintModel::Inherit,
3382 system_prompt: "Test prompt",
3383 tools: vec![],
3384 max_turns: None,
3385 config_schema: None,
3386 }]
3387 }
3388 }
3389
3390 let mut registry = CapabilityRegistry::new();
3391 registry.register(BlueprintProviderCapability);
3392
3393 let (capability_id, blueprint) = registry
3394 .blueprint_with_capability("test_blueprint")
3395 .expect("blueprint should resolve with capability id");
3396 assert_eq!(capability_id, "blueprint_provider");
3397 assert_eq!(blueprint.id, "test_blueprint");
3398 }
3399
3400 #[test]
3401 fn test_capability_registry_builder() {
3402 let registry = CapabilityRegistry::builder()
3403 .capability(NoopCapability)
3404 .capability(CurrentTimeCapability)
3405 .build();
3406
3407 assert!(registry.has("noop"));
3408 assert!(registry.has("current_time"));
3409 assert_eq!(registry.len(), 2);
3410 }
3411
3412 #[test]
3413 fn test_capability_status() {
3414 let registry = CapabilityRegistry::with_builtins();
3415
3416 let current_time = registry.get("current_time").unwrap();
3417 assert_eq!(current_time.status(), CapabilityStatus::Available);
3418
3419 let research = registry.get("research").unwrap();
3420 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3421 }
3422
3423 #[test]
3424 fn test_capability_icons_and_categories() {
3425 let registry = CapabilityRegistry::with_builtins();
3426
3427 let noop = registry.get("noop").unwrap();
3428 assert_eq!(noop.icon(), Some("circle-off"));
3429 assert_eq!(noop.category(), Some("Testing"));
3430
3431 let current_time = registry.get("current_time").unwrap();
3432 assert_eq!(current_time.icon(), Some("clock"));
3433 assert_eq!(current_time.category(), Some("Core"));
3434 }
3435
3436 #[test]
3437 fn test_system_prompt_preview_default_delegates_to_addition() {
3438 let registry = CapabilityRegistry::with_builtins();
3439
3440 let test_math = registry.get("test_math").unwrap();
3442 assert_eq!(
3443 test_math.system_prompt_preview().as_deref(),
3444 test_math.system_prompt_addition()
3445 );
3446
3447 let current_time = registry.get("current_time").unwrap();
3449 assert!(current_time.system_prompt_preview().is_none());
3450 assert!(current_time.system_prompt_addition().is_none());
3451 }
3452
3453 #[test]
3454 fn test_system_prompt_preview_dynamic_capability() {
3455 let registry = CapabilityRegistry::with_builtins();
3456 let cap = registry.get("agent_instructions").unwrap();
3457
3458 assert!(cap.system_prompt_addition().is_none());
3460 assert!(cap.system_prompt_preview().is_some());
3461 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3462 }
3463
3464 #[tokio::test]
3469 async fn test_apply_capabilities_empty() {
3470 let registry = CapabilityRegistry::with_builtins();
3471 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3472
3473 let applied =
3474 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3475
3476 assert_eq!(
3477 applied.runtime_agent.system_prompt,
3478 base_runtime_agent.system_prompt
3479 );
3480 assert!(applied.tool_registry.is_empty());
3481 assert!(applied.applied_ids.is_empty());
3482 }
3483
3484 #[tokio::test]
3485 async fn test_apply_capabilities_noop() {
3486 let registry = CapabilityRegistry::with_builtins();
3487 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3488
3489 let applied = apply_capabilities(
3490 base_runtime_agent.clone(),
3491 &["noop".to_string()],
3492 ®istry,
3493 &test_ctx(),
3494 )
3495 .await;
3496
3497 assert_eq!(
3499 applied.runtime_agent.system_prompt,
3500 base_runtime_agent.system_prompt
3501 );
3502 assert!(applied.tool_registry.is_empty());
3503 assert_eq!(applied.applied_ids, vec!["noop"]);
3504 }
3505
3506 #[tokio::test]
3507 async fn test_apply_capabilities_current_time() {
3508 let registry = CapabilityRegistry::with_builtins();
3509 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3510
3511 let applied = apply_capabilities(
3512 base_runtime_agent.clone(),
3513 &["current_time".to_string()],
3514 ®istry,
3515 &test_ctx(),
3516 )
3517 .await;
3518
3519 assert!(
3523 applied
3524 .runtime_agent
3525 .system_prompt
3526 .contains(FACTS_DYNAMIC_NOTE),
3527 "current_time should contribute the dynamic-facts note"
3528 );
3529 assert!(
3530 applied
3531 .runtime_agent
3532 .system_prompt
3533 .contains(&base_runtime_agent.system_prompt),
3534 "base prompt is preserved"
3535 );
3536 assert!(applied.tool_registry.has("get_current_time"));
3537 assert_eq!(applied.tool_registry.len(), 1);
3538 assert_eq!(applied.applied_ids, vec!["current_time"]);
3539 }
3540
3541 #[tokio::test]
3542 async fn test_apply_capabilities_skips_coming_soon() {
3543 let registry = CapabilityRegistry::with_builtins();
3544 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3545
3546 let applied = apply_capabilities(
3548 base_runtime_agent.clone(),
3549 &["research".to_string()],
3550 ®istry,
3551 &test_ctx(),
3552 )
3553 .await;
3554
3555 assert_eq!(
3557 applied.runtime_agent.system_prompt,
3558 base_runtime_agent.system_prompt
3559 );
3560 assert!(applied.applied_ids.is_empty()); }
3562
3563 #[tokio::test]
3564 async fn test_apply_capabilities_multiple() {
3565 let registry = CapabilityRegistry::with_builtins();
3566 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3567
3568 let applied = apply_capabilities(
3569 base_runtime_agent.clone(),
3570 &["noop".to_string(), "current_time".to_string()],
3571 ®istry,
3572 &test_ctx(),
3573 )
3574 .await;
3575
3576 assert!(applied.tool_registry.has("get_current_time"));
3577 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3578 }
3579
3580 #[tokio::test]
3581 async fn test_apply_capabilities_preserves_order() {
3582 let registry = CapabilityRegistry::with_builtins();
3583 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3584
3585 let applied = apply_capabilities(
3587 base_runtime_agent,
3588 &["current_time".to_string(), "noop".to_string()],
3589 ®istry,
3590 &test_ctx(),
3591 )
3592 .await;
3593
3594 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3595 }
3596
3597 #[tokio::test]
3598 async fn test_apply_capabilities_test_math() {
3599 let registry = CapabilityRegistry::with_builtins();
3600 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3601
3602 let applied = apply_capabilities(
3603 base_runtime_agent.clone(),
3604 &["test_math".to_string()],
3605 ®istry,
3606 &test_ctx(),
3607 )
3608 .await;
3609
3610 assert!(
3612 !applied
3613 .runtime_agent
3614 .system_prompt
3615 .contains("<capability id=\"test_math\">")
3616 );
3617 assert!(
3619 applied
3620 .runtime_agent
3621 .system_prompt
3622 .contains("You are a helpful assistant.")
3623 );
3624 assert!(applied.tool_registry.has("add"));
3625 assert!(applied.tool_registry.has("subtract"));
3626 assert!(applied.tool_registry.has("multiply"));
3627 assert!(applied.tool_registry.has("divide"));
3628 assert_eq!(applied.tool_registry.len(), 4);
3629 }
3630
3631 #[tokio::test]
3632 async fn test_apply_capabilities_test_weather() {
3633 let registry = CapabilityRegistry::with_builtins();
3634 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3635
3636 let applied = apply_capabilities(
3637 base_runtime_agent.clone(),
3638 &["test_weather".to_string()],
3639 ®istry,
3640 &test_ctx(),
3641 )
3642 .await;
3643
3644 assert!(
3646 !applied
3647 .runtime_agent
3648 .system_prompt
3649 .contains("<capability id=\"test_weather\">")
3650 );
3651 assert!(applied.tool_registry.has("get_weather"));
3652 assert!(applied.tool_registry.has("get_forecast"));
3653 assert_eq!(applied.tool_registry.len(), 2);
3654 }
3655
3656 #[tokio::test]
3657 async fn test_apply_capabilities_test_math_and_test_weather() {
3658 let registry = CapabilityRegistry::with_builtins();
3659 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3660
3661 let applied = apply_capabilities(
3662 base_runtime_agent.clone(),
3663 &["test_math".to_string(), "test_weather".to_string()],
3664 ®istry,
3665 &test_ctx(),
3666 )
3667 .await;
3668
3669 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3672 assert!(applied.tool_registry.has("get_weather"));
3673 }
3674
3675 #[tokio::test]
3676 async fn test_apply_capabilities_stateless_todo_list() {
3677 let registry = CapabilityRegistry::with_builtins();
3678 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3679
3680 let applied = apply_capabilities(
3681 base_runtime_agent.clone(),
3682 &["stateless_todo_list".to_string()],
3683 ®istry,
3684 &test_ctx(),
3685 )
3686 .await;
3687
3688 assert!(
3690 applied
3691 .runtime_agent
3692 .system_prompt
3693 .contains("Task Management")
3694 );
3695 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3696 assert!(applied.tool_registry.has("write_todos"));
3697 assert_eq!(applied.tool_registry.len(), 1);
3698 }
3699
3700 #[tokio::test]
3701 async fn test_apply_capabilities_web_fetch() {
3702 let registry = CapabilityRegistry::with_builtins();
3703 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3704
3705 let applied = apply_capabilities(
3706 base_runtime_agent.clone(),
3707 &["web_fetch".to_string()],
3708 ®istry,
3709 &test_ctx(),
3710 )
3711 .await;
3712
3713 assert!(
3715 applied
3716 .runtime_agent
3717 .system_prompt
3718 .contains(&base_runtime_agent.system_prompt)
3719 );
3720 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3721 assert!(applied.tool_registry.has("web_fetch"));
3722 assert_eq!(applied.tool_registry.len(), 1);
3723 }
3724
3725 #[tokio::test]
3730 async fn test_xml_tags_wrap_capability_prompts() {
3731 let registry = CapabilityRegistry::with_builtins();
3732 let collected =
3733 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3734 .await;
3735
3736 assert_eq!(collected.system_prompt_parts.len(), 1);
3737 let part = &collected.system_prompt_parts[0];
3738 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3739 assert!(part.ends_with("</capability>"));
3740 assert!(part.contains("Task Management"));
3741 }
3742
3743 #[tokio::test]
3744 async fn test_xml_tags_multiple_capabilities() {
3745 let registry = CapabilityRegistry::with_builtins();
3746 let collected = collect_capabilities(
3747 &[
3748 "stateless_todo_list".to_string(),
3749 "session_schedule".to_string(),
3750 ],
3751 ®istry,
3752 &test_ctx(),
3753 )
3754 .await;
3755
3756 assert_eq!(collected.system_prompt_parts.len(), 2);
3757 assert!(
3758 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3759 );
3760 assert!(
3761 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3762 );
3763
3764 let prefix = collected.system_prompt_prefix().unwrap();
3765 assert!(prefix.contains("</capability>\n\n<capability"));
3767 }
3768
3769 #[tokio::test]
3770 async fn test_xml_tags_system_prompt_wrapping() {
3771 let registry = CapabilityRegistry::with_builtins();
3772 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3773
3774 let applied = apply_capabilities(
3775 base,
3776 &["stateless_todo_list".to_string()],
3777 ®istry,
3778 &test_ctx(),
3779 )
3780 .await;
3781
3782 let prompt = &applied.runtime_agent.system_prompt;
3783 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3784 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3786 assert!(prompt.contains("</capability>"));
3787 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3789 }
3790
3791 #[tokio::test]
3792 async fn test_no_xml_wrapping_without_capabilities() {
3793 let registry = CapabilityRegistry::with_builtins();
3794 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3795
3796 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3797
3798 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3800 assert!(
3801 !applied
3802 .runtime_agent
3803 .system_prompt
3804 .contains("<system-prompt>")
3805 );
3806 }
3807
3808 #[tokio::test]
3809 async fn test_no_xml_wrapping_for_noop_capability() {
3810 let registry = CapabilityRegistry::with_builtins();
3811 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3812
3813 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3815
3816 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3817 assert!(
3818 !applied
3819 .runtime_agent
3820 .system_prompt
3821 .contains("<system-prompt>")
3822 );
3823 }
3824
3825 #[tokio::test]
3830 async fn test_collect_capabilities_includes_mounts() {
3831 let registry = CapabilityRegistry::with_builtins();
3832
3833 let collected =
3834 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3835
3836 assert!(!collected.mounts.is_empty());
3837 assert_eq!(collected.mounts.len(), 1);
3838 assert_eq!(collected.mounts[0].path, "/samples");
3839 assert!(collected.mounts[0].is_readonly());
3840 }
3841
3842 #[tokio::test]
3843 async fn test_collect_capabilities_empty_mounts_by_default() {
3844 let registry = CapabilityRegistry::with_builtins();
3845
3846 let collected =
3848 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3849
3850 assert!(collected.mounts.is_empty());
3851 }
3852
3853 #[tokio::test]
3854 async fn test_dynamic_facts_add_note_without_static_block() {
3855 let registry = CapabilityRegistry::with_builtins();
3859 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3860 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3861 let prompt = collected.system_prompt_parts.join("\n");
3862 assert!(
3863 prompt.contains(FACTS_DYNAMIC_NOTE),
3864 "dynamic-facts note should be in the cached prompt"
3865 );
3866 assert!(
3867 !prompt.contains("<facts>\n"),
3868 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
3869 );
3870 }
3871
3872 #[tokio::test]
3873 async fn test_static_facts_fold_into_prompt() {
3874 struct StaticFactCap;
3875 impl Capability for StaticFactCap {
3876 fn id(&self) -> &str {
3877 "test_static_fact"
3878 }
3879 fn name(&self) -> &str {
3880 "Static Fact"
3881 }
3882 fn description(&self) -> &str {
3883 "test"
3884 }
3885 fn status(&self) -> CapabilityStatus {
3886 CapabilityStatus::Available
3887 }
3888 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
3889 vec![Fact::stat("workspace_root", "/workspace")]
3890 }
3891 }
3892 let mut registry = CapabilityRegistry::new();
3893 registry.register(StaticFactCap);
3894 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
3895 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3896 let prompt = collected.system_prompt_parts.join("\n");
3897 assert!(
3898 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
3899 "static fact should fold into the cached prompt; got: {prompt}"
3900 );
3901 assert!(
3902 !prompt.contains(FACTS_DYNAMIC_NOTE),
3903 "no dynamic note when only static facts exist"
3904 );
3905 }
3906
3907 #[test]
3908 fn test_collect_dynamic_facts_returns_current_time() {
3909 let registry = CapabilityRegistry::with_builtins();
3910 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3911 let facts = collect_dynamic_facts(
3912 &configs,
3913 ®istry,
3914 None,
3915 &FactsContext::new(SessionId::new()),
3916 );
3917 assert_eq!(facts.len(), 1);
3918 assert_eq!(facts[0].key, "current_time");
3919 assert_eq!(facts[0].volatility, Volatility::Dynamic);
3920 }
3921
3922 #[tokio::test]
3923 async fn test_collect_capabilities_combines_mounts() {
3924 let registry = CapabilityRegistry::with_builtins();
3925
3926 let collected = collect_capabilities(
3929 &["sample_data".to_string(), "current_time".to_string()],
3930 ®istry,
3931 &test_ctx(),
3932 )
3933 .await;
3934
3935 assert_eq!(collected.mounts.len(), 1);
3936 assert!(
3938 collected
3939 .applied_ids
3940 .iter()
3941 .any(|id| id == "session_file_system")
3942 );
3943 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3944 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3945 }
3946
3947 #[test]
3948 fn test_sample_data_capability() {
3949 let registry = CapabilityRegistry::with_builtins();
3950 let cap = registry.get("sample_data").unwrap();
3951
3952 assert_eq!(cap.id(), "sample_data");
3953 assert_eq!(cap.name(), "Sample Data");
3954 assert_eq!(cap.status(), CapabilityStatus::Available);
3955
3956 assert!(cap.system_prompt_addition().is_some());
3958 assert!(cap.tools().is_empty());
3959
3960 assert!(!cap.mounts().is_empty());
3962 }
3963
3964 #[test]
3969 fn test_resolve_dependencies_empty() {
3970 let registry = CapabilityRegistry::with_builtins();
3971
3972 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3973
3974 assert!(resolved.resolved_ids.is_empty());
3975 assert!(resolved.added_as_dependencies.is_empty());
3976 assert!(resolved.user_selected.is_empty());
3977 }
3978
3979 #[test]
3980 fn test_resolve_dependencies_no_deps() {
3981 let registry = CapabilityRegistry::with_builtins();
3982
3983 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3985
3986 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3987 assert!(resolved.added_as_dependencies.is_empty());
3988 }
3989
3990 #[test]
3991 fn test_resolve_dependencies_with_deps() {
3992 let registry = CapabilityRegistry::with_builtins();
3993
3994 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3996
3997 assert_eq!(resolved.resolved_ids.len(), 2);
3999 let fs_pos = resolved
4000 .resolved_ids
4001 .iter()
4002 .position(|id| id == "session_file_system")
4003 .unwrap();
4004 let sd_pos = resolved
4005 .resolved_ids
4006 .iter()
4007 .position(|id| id == "sample_data")
4008 .unwrap();
4009 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4010
4011 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4013 }
4014
4015 #[test]
4016 fn test_resolve_dependencies_already_selected() {
4017 let registry = CapabilityRegistry::with_builtins();
4018
4019 let resolved = resolve_dependencies(
4021 &["session_file_system".to_string(), "sample_data".to_string()],
4022 ®istry,
4023 )
4024 .unwrap();
4025
4026 assert_eq!(resolved.resolved_ids.len(), 2);
4027 assert!(resolved.added_as_dependencies.is_empty());
4029 }
4030
4031 #[test]
4032 fn test_resolve_dependencies_preserves_order() {
4033 let registry = CapabilityRegistry::with_builtins();
4034
4035 let resolved =
4037 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4038 .unwrap();
4039
4040 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4041 }
4042
4043 #[test]
4044 fn test_resolve_dependencies_unknown_capability() {
4045 let registry = CapabilityRegistry::with_builtins();
4046
4047 let resolved =
4049 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4050
4051 assert!(resolved.resolved_ids.is_empty());
4052 }
4053
4054 #[test]
4055 fn test_get_dependencies() {
4056 let registry = CapabilityRegistry::with_builtins();
4057
4058 let deps = get_dependencies("sample_data", ®istry);
4060 assert_eq!(deps, vec!["session_file_system"]);
4061
4062 let deps = get_dependencies("current_time", ®istry);
4064 assert!(deps.is_empty());
4065
4066 let deps = get_dependencies("unknown", ®istry);
4068 assert!(deps.is_empty());
4069 }
4070
4071 #[test]
4072 fn test_sample_data_has_dependency() {
4073 let registry = CapabilityRegistry::with_builtins();
4074 let cap = registry.get("sample_data").unwrap();
4075
4076 let deps = cap.dependencies();
4077 assert_eq!(deps.len(), 1);
4078 assert_eq!(deps[0], "session_file_system");
4079 }
4080
4081 #[test]
4082 fn test_noop_has_no_dependencies() {
4083 let registry = CapabilityRegistry::with_builtins();
4084 let cap = registry.get("noop").unwrap();
4085
4086 assert!(cap.dependencies().is_empty());
4087 }
4088
4089 #[test]
4093 fn test_circular_dependency_error() {
4094 struct CapA;
4096 struct CapB;
4097
4098 impl Capability for CapA {
4099 fn id(&self) -> &str {
4100 "test_cap_a"
4101 }
4102 fn name(&self) -> &str {
4103 "Test A"
4104 }
4105 fn description(&self) -> &str {
4106 "Test capability A"
4107 }
4108 fn dependencies(&self) -> Vec<&'static str> {
4109 vec!["test_cap_b"]
4110 }
4111 }
4112
4113 impl Capability for CapB {
4114 fn id(&self) -> &str {
4115 "test_cap_b"
4116 }
4117 fn name(&self) -> &str {
4118 "Test B"
4119 }
4120 fn description(&self) -> &str {
4121 "Test capability B"
4122 }
4123 fn dependencies(&self) -> Vec<&'static str> {
4124 vec!["test_cap_a"]
4125 }
4126 }
4127
4128 let mut registry = CapabilityRegistry::new();
4129 registry.register(CapA);
4130 registry.register(CapB);
4131
4132 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4133
4134 assert!(result.is_err());
4135 match result.unwrap_err() {
4136 DependencyError::CircularDependency { capability_id, .. } => {
4137 assert_eq!(capability_id, "test_cap_a");
4138 }
4139 _ => panic!("Expected CircularDependency error"),
4140 }
4141 }
4142
4143 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4148
4149 struct FilterTestCapability {
4151 priority: i32,
4152 }
4153
4154 impl Capability for FilterTestCapability {
4155 fn id(&self) -> &str {
4156 "filter_test"
4157 }
4158 fn name(&self) -> &str {
4159 "Filter Test"
4160 }
4161 fn description(&self) -> &str {
4162 "Test capability with message filter"
4163 }
4164 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4165 Some(Arc::new(FilterTestProvider {
4166 priority: self.priority,
4167 }))
4168 }
4169 }
4170
4171 struct FilterTestProvider {
4172 priority: i32,
4173 }
4174
4175 impl MessageFilterProvider for FilterTestProvider {
4176 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4177 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4179 query
4180 .filters
4181 .push(MessageFilter::Search(search.to_string()));
4182 }
4183 }
4184
4185 fn priority(&self) -> i32 {
4186 self.priority
4187 }
4188 }
4189
4190 #[tokio::test]
4191 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4192 let registry = CapabilityRegistry::with_builtins();
4193 let configs = vec![AgentCapabilityConfig {
4194 capability_ref: CapabilityId::new("current_time"),
4195 config: serde_json::json!({}),
4196 }];
4197
4198 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4199
4200 assert!(collected.message_filter_providers.is_empty());
4201 assert!(!collected.has_message_filters());
4202 }
4203
4204 #[tokio::test]
4205 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4206 let mut registry = CapabilityRegistry::new();
4207 registry.register(FilterTestCapability { priority: 0 });
4208
4209 let configs = vec![AgentCapabilityConfig {
4210 capability_ref: CapabilityId::new("filter_test"),
4211 config: serde_json::json!({ "search": "hello" }),
4212 }];
4213
4214 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4215
4216 assert_eq!(collected.message_filter_providers.len(), 1);
4217 assert!(collected.has_message_filters());
4218 }
4219
4220 #[tokio::test]
4221 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4222 struct HighPriorityCapability;
4224 struct LowPriorityCapability;
4225
4226 impl Capability for HighPriorityCapability {
4227 fn id(&self) -> &str {
4228 "high_priority"
4229 }
4230 fn name(&self) -> &str {
4231 "High Priority"
4232 }
4233 fn description(&self) -> &str {
4234 "Test"
4235 }
4236 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4237 Some(Arc::new(FilterTestProvider { priority: 10 }))
4238 }
4239 }
4240
4241 impl Capability for LowPriorityCapability {
4242 fn id(&self) -> &str {
4243 "low_priority"
4244 }
4245 fn name(&self) -> &str {
4246 "Low Priority"
4247 }
4248 fn description(&self) -> &str {
4249 "Test"
4250 }
4251 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4252 Some(Arc::new(FilterTestProvider { priority: -5 }))
4253 }
4254 }
4255
4256 let mut registry = CapabilityRegistry::new();
4257 registry.register(HighPriorityCapability);
4258 registry.register(LowPriorityCapability);
4259
4260 let configs = vec![
4262 AgentCapabilityConfig {
4263 capability_ref: CapabilityId::new("high_priority"),
4264 config: serde_json::json!({}),
4265 },
4266 AgentCapabilityConfig {
4267 capability_ref: CapabilityId::new("low_priority"),
4268 config: serde_json::json!({}),
4269 },
4270 ];
4271
4272 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4273
4274 assert_eq!(collected.message_filter_providers.len(), 2);
4276 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4277 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4278 }
4279
4280 #[tokio::test]
4281 async fn test_collected_capabilities_apply_message_filters() {
4282 let mut registry = CapabilityRegistry::new();
4283 registry.register(FilterTestCapability { priority: 0 });
4284
4285 let configs = vec![AgentCapabilityConfig {
4286 capability_ref: CapabilityId::new("filter_test"),
4287 config: serde_json::json!({ "search": "test_query" }),
4288 }];
4289
4290 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4291
4292 let session_id: SessionId = Uuid::now_v7().into();
4294 let mut query = MessageQuery::new(session_id);
4295
4296 collected.apply_message_filters(&mut query);
4297
4298 assert_eq!(query.filters.len(), 1);
4300 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4301 }
4302
4303 #[tokio::test]
4304 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4305 struct SearchCapability {
4306 id: &'static str,
4307 search_term: &'static str,
4308 priority: i32,
4309 }
4310
4311 struct SearchProvider {
4312 search_term: &'static str,
4313 priority: i32,
4314 }
4315
4316 impl MessageFilterProvider for SearchProvider {
4317 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4318 query
4319 .filters
4320 .push(MessageFilter::Search(self.search_term.to_string()));
4321 }
4322
4323 fn priority(&self) -> i32 {
4324 self.priority
4325 }
4326 }
4327
4328 impl Capability for SearchCapability {
4329 fn id(&self) -> &str {
4330 self.id
4331 }
4332 fn name(&self) -> &str {
4333 "Search"
4334 }
4335 fn description(&self) -> &str {
4336 "Test"
4337 }
4338 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4339 Some(Arc::new(SearchProvider {
4340 search_term: self.search_term,
4341 priority: self.priority,
4342 }))
4343 }
4344 }
4345
4346 let mut registry = CapabilityRegistry::new();
4347 registry.register(SearchCapability {
4348 id: "cap_a",
4349 search_term: "alpha",
4350 priority: 5,
4351 });
4352 registry.register(SearchCapability {
4353 id: "cap_b",
4354 search_term: "beta",
4355 priority: 1,
4356 });
4357 registry.register(SearchCapability {
4358 id: "cap_c",
4359 search_term: "gamma",
4360 priority: 10,
4361 });
4362
4363 let configs = vec![
4364 AgentCapabilityConfig {
4365 capability_ref: CapabilityId::new("cap_a"),
4366 config: serde_json::json!({}),
4367 },
4368 AgentCapabilityConfig {
4369 capability_ref: CapabilityId::new("cap_b"),
4370 config: serde_json::json!({}),
4371 },
4372 AgentCapabilityConfig {
4373 capability_ref: CapabilityId::new("cap_c"),
4374 config: serde_json::json!({}),
4375 },
4376 ];
4377
4378 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4379
4380 let session_id: SessionId = Uuid::now_v7().into();
4381 let mut query = MessageQuery::new(session_id);
4382
4383 collected.apply_message_filters(&mut query);
4384
4385 assert_eq!(query.filters.len(), 3);
4387 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4388 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4389 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4390 }
4391
4392 #[test]
4393 fn test_capability_without_message_filter_returns_none() {
4394 let registry = CapabilityRegistry::with_builtins();
4395
4396 let noop = registry.get("noop").unwrap();
4397 assert!(noop.message_filter_provider().is_none());
4398
4399 let current_time = registry.get("current_time").unwrap();
4400 assert!(current_time.message_filter_provider().is_none());
4401 }
4402
4403 #[tokio::test]
4404 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4405 let mut registry = CapabilityRegistry::new();
4406 registry.register(FilterTestCapability { priority: 0 });
4407
4408 let test_config = serde_json::json!({
4409 "search": "custom_search",
4410 "extra_field": 42
4411 });
4412
4413 let configs = vec![AgentCapabilityConfig {
4414 capability_ref: CapabilityId::new("filter_test"),
4415 config: test_config.clone(),
4416 }];
4417
4418 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4419
4420 assert_eq!(collected.message_filter_providers.len(), 1);
4422 let (_, stored_config) = &collected.message_filter_providers[0];
4423 assert_eq!(*stored_config, test_config);
4424 }
4425
4426 #[test]
4431 fn test_collect_message_filters_only_collects_filters() {
4432 let mut registry = CapabilityRegistry::new();
4433 registry.register(FilterTestCapability { priority: 0 });
4434
4435 let configs = vec![AgentCapabilityConfig {
4436 capability_ref: CapabilityId::new("filter_test"),
4437 config: serde_json::json!({ "search": "test_query" }),
4438 }];
4439
4440 let collected = collect_message_filters_only(&configs, ®istry);
4441
4442 let session_id: SessionId = Uuid::now_v7().into();
4443 let mut query = MessageQuery::new(session_id);
4444 collected.apply_message_filters(&mut query);
4445
4446 assert_eq!(query.filters.len(), 1);
4447 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4448 }
4449
4450 #[test]
4451 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4452 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4453
4454 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4456 assert_eq!(with["compaction_active"], serde_json::json!(true));
4457 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4458
4459 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4460 assert!(without.get("compaction_active").is_none());
4461
4462 let other = message_filter_config_for("other", &base, true);
4464 assert!(other.get("compaction_active").is_none());
4465
4466 let null_base = message_filter_config_for(
4468 INFINITY_CONTEXT_CAPABILITY_ID,
4469 &serde_json::Value::Null,
4470 true,
4471 );
4472 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4473 }
4474
4475 #[test]
4476 fn test_infinity_context_defers_to_compaction_end_to_end() {
4477 use crate::message::Message;
4478
4479 let mut registry = CapabilityRegistry::new();
4480 registry.register(InfinityContextCapability);
4481 registry.register(CompactionCapability);
4482
4483 let tight = serde_json::json!({
4484 "context_budget_tokens": 1,
4485 "min_recent_messages": 1
4486 });
4487
4488 let solo = vec![AgentCapabilityConfig {
4490 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4491 config: tight.clone(),
4492 }];
4493 let mut messages = vec![
4494 Message::user("task"),
4495 Message::assistant("old ".repeat(400)),
4496 Message::user("recent"),
4497 ];
4498 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4499 assert!(
4500 messages
4501 .iter()
4502 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4503 "infinity context alone should trim and notice"
4504 );
4505
4506 let both = vec![
4508 AgentCapabilityConfig {
4509 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4510 config: tight,
4511 },
4512 AgentCapabilityConfig {
4513 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4514 config: serde_json::json!({}),
4515 },
4516 ];
4517 let mut messages = vec![
4518 Message::user("task"),
4519 Message::assistant("old ".repeat(400)),
4520 Message::user("recent"),
4521 ];
4522 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4523 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4524 assert!(
4525 messages
4526 .iter()
4527 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4528 "no hidden-history notice when compaction is the active reducer"
4529 );
4530 }
4531
4532 #[test]
4533 fn test_compaction_is_enabled_detects_compaction() {
4534 let mut registry = CapabilityRegistry::new();
4535 registry.register(CompactionCapability);
4536
4537 let with_compaction = vec![AgentCapabilityConfig {
4538 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4539 config: serde_json::json!({}),
4540 }];
4541 assert!(compaction_is_enabled(&with_compaction, ®istry));
4542
4543 let without = vec![AgentCapabilityConfig {
4544 capability_ref: CapabilityId::new("current_time"),
4545 config: serde_json::json!({}),
4546 }];
4547 assert!(!compaction_is_enabled(&without, ®istry));
4548 }
4549
4550 #[test]
4551 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4552 let registry = CapabilityRegistry::new();
4553
4554 let configs = vec![AgentCapabilityConfig {
4555 capability_ref: CapabilityId::new("nonexistent"),
4556 config: serde_json::json!({}),
4557 }];
4558
4559 let collected = collect_message_filters_only(&configs, ®istry);
4560 assert!(collected.message_filter_providers.is_empty());
4561 }
4562
4563 #[test]
4564 fn test_collect_message_filters_only_preserves_priority_order() {
4565 struct PriorityFilterCap {
4566 id: &'static str,
4567 search_term: &'static str,
4568 priority: i32,
4569 }
4570
4571 struct PriorityFilterProvider {
4572 search_term: &'static str,
4573 priority: i32,
4574 }
4575
4576 impl Capability for PriorityFilterCap {
4577 fn id(&self) -> &str {
4578 self.id
4579 }
4580 fn name(&self) -> &str {
4581 self.id
4582 }
4583 fn description(&self) -> &str {
4584 "priority test"
4585 }
4586 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4587 Some(Arc::new(PriorityFilterProvider {
4588 search_term: self.search_term,
4589 priority: self.priority,
4590 }))
4591 }
4592 }
4593
4594 impl MessageFilterProvider for PriorityFilterProvider {
4595 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4596 query
4597 .filters
4598 .push(MessageFilter::Search(self.search_term.to_string()));
4599 }
4600 fn priority(&self) -> i32 {
4601 self.priority
4602 }
4603 }
4604
4605 let mut registry = CapabilityRegistry::new();
4606 registry.register(PriorityFilterCap {
4607 id: "gamma",
4608 search_term: "gamma",
4609 priority: 10,
4610 });
4611 registry.register(PriorityFilterCap {
4612 id: "alpha",
4613 search_term: "alpha",
4614 priority: 5,
4615 });
4616 registry.register(PriorityFilterCap {
4617 id: "beta",
4618 search_term: "beta",
4619 priority: 1,
4620 });
4621
4622 let configs = vec![
4623 AgentCapabilityConfig {
4624 capability_ref: CapabilityId::new("gamma"),
4625 config: serde_json::json!({}),
4626 },
4627 AgentCapabilityConfig {
4628 capability_ref: CapabilityId::new("alpha"),
4629 config: serde_json::json!({}),
4630 },
4631 AgentCapabilityConfig {
4632 capability_ref: CapabilityId::new("beta"),
4633 config: serde_json::json!({}),
4634 },
4635 ];
4636
4637 let collected = collect_message_filters_only(&configs, ®istry);
4638
4639 let session_id: SessionId = Uuid::now_v7().into();
4640 let mut query = MessageQuery::new(session_id);
4641 collected.apply_message_filters(&mut query);
4642
4643 assert_eq!(query.filters.len(), 3);
4645 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4646 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4647 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4648 }
4649
4650 #[test]
4651 fn test_collect_message_filters_only_post_load_invoked() {
4652 use crate::message::Message;
4653
4654 struct PostLoadCap;
4655 struct PostLoadProvider;
4656
4657 impl Capability for PostLoadCap {
4658 fn id(&self) -> &str {
4659 "post_load_test"
4660 }
4661 fn name(&self) -> &str {
4662 "PostLoad Test"
4663 }
4664 fn description(&self) -> &str {
4665 "test"
4666 }
4667 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4668 Some(Arc::new(PostLoadProvider))
4669 }
4670 }
4671
4672 impl MessageFilterProvider for PostLoadProvider {
4673 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4674 fn priority(&self) -> i32 {
4675 0
4676 }
4677 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4678 messages.reverse();
4680 }
4681 }
4682
4683 let mut registry = CapabilityRegistry::new();
4684 registry.register(PostLoadCap);
4685
4686 let configs = vec![AgentCapabilityConfig {
4687 capability_ref: CapabilityId::new("post_load_test"),
4688 config: serde_json::json!({}),
4689 }];
4690
4691 let collected = collect_message_filters_only(&configs, ®istry);
4692
4693 let mut messages = vec![Message::user("first"), Message::user("second")];
4694 collected.apply_post_load_filters(&mut messages);
4695
4696 assert_eq!(messages[0].text(), Some("second"));
4698 assert_eq!(messages[1].text(), Some("first"));
4699 }
4700
4701 #[test]
4702 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4703 use crate::tool_types::ToolCall;
4704
4705 fn tool_heavy_messages() -> Vec<Message> {
4706 let mut messages = vec![Message::user("inspect files repeatedly")];
4707 for index in 0..9 {
4708 let call_id = format!("call_{index}");
4709 messages.push(Message::assistant_with_tools(
4710 "",
4711 vec![ToolCall {
4712 id: call_id.clone(),
4713 name: "read_file".to_string(),
4714 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4715 }],
4716 ));
4717 messages.push(Message::tool_result(
4718 call_id,
4719 Some(serde_json::json!({
4720 "path": "/workspace/src/lib.rs",
4721 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4722 "total_lines": 1000,
4723 "lines_shown": {"start": 1, "end": 1000},
4724 "truncated": false
4725 })),
4726 None,
4727 ));
4728 }
4729 messages
4730 }
4731
4732 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4733 messages[2]
4734 .tool_result_content()
4735 .and_then(|result| result.result.as_ref())
4736 .and_then(|result| result.get("masked"))
4737 .and_then(|masked| masked.as_bool())
4738 .unwrap_or(false)
4739 }
4740
4741 let mut registry = CapabilityRegistry::new();
4742 registry.register(CompactionCapability);
4743 let context = ModelViewContext {
4744 session_id: SessionId::new(),
4745 prior_usage: None,
4746 };
4747
4748 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4749 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4750 assert!(!first_tool_result_is_masked(&unmasked));
4751
4752 let compaction = collect_model_view_providers(
4753 &[AgentCapabilityConfig {
4754 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4755 config: serde_json::json!({}),
4756 }],
4757 ®istry,
4758 None,
4759 );
4760 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4761 assert!(first_tool_result_is_masked(&masked));
4762 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4763 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4764 }
4765
4766 struct DelegatingFilterCap {
4769 id: &'static str,
4770 inner: std::sync::Arc<InnerFilterCap>,
4771 }
4772 struct InnerFilterCap;
4773
4774 impl Capability for InnerFilterCap {
4775 fn id(&self) -> &str {
4776 "inner_filter"
4777 }
4778 fn name(&self) -> &str {
4779 "Inner Filter"
4780 }
4781 fn description(&self) -> &str {
4782 "inner"
4783 }
4784 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4785 Some(std::sync::Arc::new(SentinelFilter))
4786 }
4787 }
4788 struct SentinelFilter;
4789 impl MessageFilterProvider for SentinelFilter {
4790 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4791 }
4792 impl Capability for DelegatingFilterCap {
4793 fn id(&self) -> &str {
4794 self.id
4795 }
4796 fn name(&self) -> &str {
4797 "Delegating Filter"
4798 }
4799 fn description(&self) -> &str {
4800 "delegating"
4801 }
4802 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4803 None }
4805 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4806 Some(&*self.inner)
4807 }
4808 }
4809
4810 #[test]
4811 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4812 let inner = std::sync::Arc::new(InnerFilterCap);
4813 let outer = DelegatingFilterCap {
4814 id: "delegating_filter",
4815 inner: inner.clone(),
4816 };
4817
4818 let mut registry = CapabilityRegistry::new();
4819 registry.register(outer);
4820
4821 let configs = vec![AgentCapabilityConfig {
4822 capability_ref: CapabilityId::new("delegating_filter"),
4823 config: serde_json::json!({}),
4824 }];
4825
4826 let collected = collect_message_filters_only(&configs, ®istry);
4829 assert_eq!(
4830 collected.message_filter_providers.len(),
4831 1,
4832 "provider from resolved inner capability must be collected"
4833 );
4834 }
4835
4836 struct DelegatingMvpCap {
4837 id: &'static str,
4838 inner: std::sync::Arc<InnerMvpCap>,
4839 }
4840 struct InnerMvpCap;
4841
4842 impl Capability for InnerMvpCap {
4843 fn id(&self) -> &str {
4844 "inner_mvp"
4845 }
4846 fn name(&self) -> &str {
4847 "Inner MVP"
4848 }
4849 fn description(&self) -> &str {
4850 "inner"
4851 }
4852 fn model_view_provider(
4853 &self,
4854 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4855 struct NoopMvp;
4857 impl crate::capabilities::ModelViewProvider for NoopMvp {
4858 fn apply_model_view(
4859 &self,
4860 messages: Vec<Message>,
4861 _config: &serde_json::Value,
4862 _context: &ModelViewContext<'_>,
4863 ) -> Vec<Message> {
4864 messages
4865 }
4866 }
4867 Some(std::sync::Arc::new(NoopMvp))
4868 }
4869 }
4870 impl Capability for DelegatingMvpCap {
4871 fn id(&self) -> &str {
4872 self.id
4873 }
4874 fn name(&self) -> &str {
4875 "Delegating MVP"
4876 }
4877 fn description(&self) -> &str {
4878 "delegating"
4879 }
4880 fn model_view_provider(
4881 &self,
4882 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4883 None }
4885 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4886 Some(&*self.inner)
4887 }
4888 }
4889
4890 #[test]
4891 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4892 let inner = std::sync::Arc::new(InnerMvpCap);
4893 let outer = DelegatingMvpCap {
4894 id: "delegating_mvp",
4895 inner: inner.clone(),
4896 };
4897
4898 let mut registry = CapabilityRegistry::new();
4899 registry.register(outer);
4900
4901 let configs = vec![AgentCapabilityConfig {
4902 capability_ref: CapabilityId::new("delegating_mvp"),
4903 config: serde_json::json!({}),
4904 }];
4905
4906 let collected = collect_model_view_providers(&configs, ®istry, None);
4909 assert_eq!(
4910 collected.model_view_providers.len(),
4911 1,
4912 "provider from resolved inner capability must be collected"
4913 );
4914 }
4915
4916 #[tokio::test]
4926 async fn test_bashkit_shell_capability_produces_bash_tool() {
4927 let registry = CapabilityRegistry::with_builtins();
4928 let collected =
4929 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4930
4931 let tool_names: Vec<&str> = collected
4932 .tool_definitions
4933 .iter()
4934 .map(|t| t.name())
4935 .collect();
4936 assert!(
4937 tool_names.contains(&"bash"),
4938 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4939 tool_names
4940 );
4941 assert!(
4942 !collected.tools.is_empty(),
4943 "bashkit_shell must provide tool implementations"
4944 );
4945 }
4946
4947 #[tokio::test]
4948 async fn test_generic_harness_capability_set_produces_bash_tool() {
4949 let generic_harness_caps = vec![
4952 "session_file_system".to_string(),
4953 "bashkit_shell".to_string(),
4954 "web_fetch".to_string(),
4955 "session_storage".to_string(),
4956 "session".to_string(),
4957 "agent_instructions".to_string(),
4958 "skills".to_string(),
4959 "infinity_context".to_string(),
4960 "auto_tool_search".to_string(),
4961 ];
4962
4963 let registry = CapabilityRegistry::with_builtins();
4964 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4965
4966 let tool_names: Vec<&str> = collected
4967 .tool_definitions
4968 .iter()
4969 .map(|t| t.name())
4970 .collect();
4971 assert!(
4972 tool_names.contains(&"bash"),
4973 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4974 tool_names
4975 );
4976 }
4977
4978 #[tokio::test]
4979 async fn test_collect_capabilities_tool_count_matches_definitions() {
4980 let registry = CapabilityRegistry::with_builtins();
4983 let collected =
4984 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4985
4986 assert_eq!(
4987 collected.tools.len(),
4988 collected.tool_definitions.len(),
4989 "tool implementations ({}) must match tool definitions ({})",
4990 collected.tools.len(),
4991 collected.tool_definitions.len(),
4992 );
4993 }
4994
4995 #[tokio::test]
4999 async fn test_collect_capabilities_resolves_dependencies() {
5000 let registry = CapabilityRegistry::with_builtins();
5003 let collected =
5004 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5005
5006 assert!(
5008 collected
5009 .applied_ids
5010 .iter()
5011 .any(|id| id == "session_file_system"),
5012 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5013 collected.applied_ids
5014 );
5015
5016 let tool_names: Vec<&str> = collected
5017 .tool_definitions
5018 .iter()
5019 .map(|t| t.name())
5020 .collect();
5021
5022 assert!(
5024 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5025 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5026 tool_names
5027 );
5028
5029 assert_eq!(
5031 collected.tools.len(),
5032 collected.tool_definitions.len(),
5033 "dependency-added tools must have implementations, not just definitions"
5034 );
5035 }
5036
5037 #[test]
5038 fn test_defaults_do_not_include_bash() {
5039 let registry = crate::ToolRegistry::with_defaults();
5042 assert!(
5043 !registry.has("bash"),
5044 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5045 );
5046 }
5047
5048 #[tokio::test]
5055 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5056 let registry = CapabilityRegistry::with_builtins();
5057 let collected =
5058 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5059
5060 let tool_names: Vec<&str> = collected
5061 .tool_definitions
5062 .iter()
5063 .map(|t| t.name())
5064 .collect();
5065 assert!(
5066 tool_names.contains(&"spawn_background"),
5067 "spawn_background must be auto-activated when bashkit_shell (a \
5068 background-capable tool) is in the agent's capability set; got: {:?}",
5069 tool_names
5070 );
5071 assert!(
5072 collected
5073 .applied_ids
5074 .iter()
5075 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5076 "background_execution must be in applied_ids when auto-activated; \
5077 got: {:?}",
5078 collected.applied_ids
5079 );
5080
5081 assert!(
5083 collected
5084 .tools
5085 .iter()
5086 .any(|t| t.name() == "spawn_background"),
5087 "spawn_background tool implementation must be present alongside the \
5088 definition (lockstep contract)"
5089 );
5090 }
5091
5092 #[tokio::test]
5095 async fn test_background_execution_does_not_auto_activate_without_hint() {
5096 let registry = CapabilityRegistry::with_builtins();
5097 let collected =
5099 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5100
5101 let tool_names: Vec<&str> = collected
5102 .tool_definitions
5103 .iter()
5104 .map(|t| t.name())
5105 .collect();
5106 assert!(
5107 !tool_names.contains(&"spawn_background"),
5108 "spawn_background must NOT be activated without a background-capable \
5109 tool; got: {:?}",
5110 tool_names
5111 );
5112 assert!(
5113 !collected
5114 .applied_ids
5115 .iter()
5116 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5117 "background_execution must not appear in applied_ids when no \
5118 background-capable tool is present; got: {:?}",
5119 collected.applied_ids
5120 );
5121 }
5122
5123 #[tokio::test]
5124 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5125 let registry = CapabilityRegistry::with_builtins();
5126 let collected = collect_capabilities(
5127 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5128 ®istry,
5129 &test_ctx(),
5130 )
5131 .await;
5132
5133 assert!(
5134 collected
5135 .tools
5136 .iter()
5137 .any(|tool| tool.name() == "spawn_agent"),
5138 "subagent-only sessions should get the unified spawn_agent adapter"
5139 );
5140 let spawn_agent = collected
5141 .tool_definitions
5142 .iter()
5143 .find(|tool| tool.name() == "spawn_agent")
5144 .expect("spawn_agent definition");
5145 assert_eq!(
5146 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5147 serde_json::json!(["subagent"])
5148 );
5149 }
5150
5151 #[tokio::test]
5152 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5153 let mut registry = CapabilityRegistry::new();
5154 registry.register(AgentHandoffCapability);
5155 let agent_id = crate::typed_id::AgentId::new();
5156 let harness_id = crate::typed_id::HarnessId::new();
5157 let configs = vec![AgentCapabilityConfig {
5158 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5159 config: serde_json::json!({
5160 "targets": [{
5161 "id": "aws_operator",
5162 "name": "AWS Operator",
5163 "agent_id": agent_id,
5164 "harness_id": harness_id
5165 }]
5166 }),
5167 }];
5168 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5169
5170 assert!(
5171 collected
5172 .tools
5173 .iter()
5174 .any(|tool| tool.name() == "spawn_agent"),
5175 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5176 );
5177 let spawn_agent = collected
5178 .tool_definitions
5179 .iter()
5180 .find(|tool| tool.name() == "spawn_agent")
5181 .expect("spawn_agent definition");
5182 assert_eq!(
5183 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5184 serde_json::json!(["agent"])
5185 );
5186 }
5187
5188 #[tokio::test]
5189 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5190 let mut registry = CapabilityRegistry::new();
5191 registry.register(SubagentCapability);
5192 registry.register(AgentHandoffCapability);
5193
5194 let agent_id = crate::typed_id::AgentId::new();
5195 let harness_id = crate::typed_id::HarnessId::new();
5196 let configs = vec![
5197 AgentCapabilityConfig {
5198 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5199 config: serde_json::json!({}),
5200 },
5201 AgentCapabilityConfig {
5202 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5203 config: serde_json::json!({
5204 "targets": [{
5205 "id": "aws_operator",
5206 "name": "AWS Operator",
5207 "agent_id": agent_id,
5208 "harness_id": harness_id
5209 }]
5210 }),
5211 },
5212 ];
5213
5214 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5215 let spawn_agent_defs: Vec<_> = collected
5216 .tool_definitions
5217 .iter()
5218 .filter(|tool| tool.name() == "spawn_agent")
5219 .collect();
5220
5221 assert_eq!(spawn_agent_defs.len(), 1);
5222 assert_eq!(
5223 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5224 serde_json::json!(["subagent", "agent"])
5225 );
5226 }
5227
5228 #[cfg(feature = "a2a")]
5229 #[tokio::test]
5230 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5231 let mut registry = CapabilityRegistry::new();
5232 registry.register(SubagentCapability);
5233 registry.register(A2aAgentDelegationCapability);
5234
5235 let configs = vec![
5236 AgentCapabilityConfig {
5237 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5238 config: serde_json::json!({}),
5239 },
5240 AgentCapabilityConfig {
5241 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5242 config: serde_json::json!({
5243 "agents": [{
5244 "id": "local_app",
5245 "name": "Local App",
5246 "base_url": "https://example.com"
5247 }]
5248 }),
5249 },
5250 ];
5251
5252 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5253 let spawn_agent_defs: Vec<_> = collected
5254 .tool_definitions
5255 .iter()
5256 .filter(|tool| tool.name() == "spawn_agent")
5257 .collect();
5258
5259 assert_eq!(spawn_agent_defs.len(), 1);
5260 assert_eq!(
5261 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5262 serde_json::json!(["subagent", "external_a2a"])
5263 );
5264 }
5265
5266 struct ExistingSpawnAgentCapability;
5267
5268 impl Capability for ExistingSpawnAgentCapability {
5269 fn id(&self) -> &str {
5270 "existing_spawn_agent"
5271 }
5272
5273 fn name(&self) -> &str {
5274 "Existing Spawn Agent"
5275 }
5276
5277 fn description(&self) -> &str {
5278 "Test capability that already owns spawn_agent"
5279 }
5280
5281 fn tools(&self) -> Vec<Box<dyn Tool>> {
5282 vec![Box::new(ExistingSpawnAgentTool)]
5283 }
5284 }
5285
5286 struct ExistingSpawnAgentTool;
5287
5288 #[async_trait]
5289 impl Tool for ExistingSpawnAgentTool {
5290 fn name(&self) -> &str {
5291 "spawn_agent"
5292 }
5293
5294 fn description(&self) -> &str {
5295 "Existing spawn_agent test tool"
5296 }
5297
5298 fn parameters_schema(&self) -> serde_json::Value {
5299 serde_json::json!({
5300 "type": "object",
5301 "properties": {
5302 "target": {
5303 "type": "object",
5304 "properties": {
5305 "type": {"type": "string", "enum": ["external_a2a"]}
5306 },
5307 "required": ["type"]
5308 }
5309 },
5310 "required": ["target"]
5311 })
5312 }
5313
5314 async fn execute(
5315 &self,
5316 _arguments: serde_json::Value,
5317 ) -> crate::tools::ToolExecutionResult {
5318 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5319 }
5320 }
5321
5322 #[tokio::test]
5323 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5324 let mut registry = CapabilityRegistry::new();
5325 registry.register(SubagentCapability);
5326 registry.register(ExistingSpawnAgentCapability);
5327
5328 let collected = collect_capabilities(
5329 &[
5330 SUBAGENTS_CAPABILITY_ID.to_string(),
5331 "existing_spawn_agent".to_string(),
5332 ],
5333 ®istry,
5334 &test_ctx(),
5335 )
5336 .await;
5337
5338 let spawn_agent_defs: Vec<_> = collected
5339 .tool_definitions
5340 .iter()
5341 .filter(|tool| tool.name() == "spawn_agent")
5342 .collect();
5343 assert_eq!(spawn_agent_defs.len(), 1);
5344 assert_eq!(
5345 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5346 serde_json::json!(["external_a2a"])
5347 );
5348 }
5349
5350 #[tokio::test]
5351 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5352 let mut registry = CapabilityRegistry::new();
5353 registry.register(AgentHandoffCapability);
5354 registry.register(ExistingSpawnAgentCapability);
5355
5356 let agent_id = crate::typed_id::AgentId::new();
5357 let harness_id = crate::typed_id::HarnessId::new();
5358 let configs = vec![
5359 AgentCapabilityConfig {
5360 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5361 config: serde_json::json!({
5362 "targets": [{
5363 "id": "aws_operator",
5364 "name": "AWS Operator",
5365 "agent_id": agent_id,
5366 "harness_id": harness_id
5367 }]
5368 }),
5369 },
5370 AgentCapabilityConfig {
5371 capability_ref: CapabilityId::new("existing_spawn_agent"),
5372 config: serde_json::json!({}),
5373 },
5374 ];
5375
5376 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5377
5378 let spawn_agent_defs: Vec<_> = collected
5379 .tool_definitions
5380 .iter()
5381 .filter(|tool| tool.name() == "spawn_agent")
5382 .collect();
5383 assert_eq!(spawn_agent_defs.len(), 1);
5384 assert_eq!(
5385 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5386 serde_json::json!(["external_a2a"])
5387 );
5388 }
5389
5390 #[tokio::test]
5394 async fn test_background_execution_explicit_selection_is_idempotent() {
5395 let registry = CapabilityRegistry::with_builtins();
5396 let collected = collect_capabilities(
5397 &[
5398 "bashkit_shell".to_string(),
5399 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5400 ],
5401 ®istry,
5402 &test_ctx(),
5403 )
5404 .await;
5405
5406 let spawn_background_count = collected
5407 .tool_definitions
5408 .iter()
5409 .filter(|t| t.name() == "spawn_background")
5410 .count();
5411 assert_eq!(
5412 spawn_background_count, 1,
5413 "spawn_background must appear exactly once even when \
5414 background_execution is selected explicitly alongside a \
5415 background-capable tool"
5416 );
5417 let applied_count = collected
5418 .applied_ids
5419 .iter()
5420 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5421 .count();
5422 assert_eq!(
5423 applied_count, 1,
5424 "background_execution must appear exactly once in applied_ids"
5425 );
5426 }
5427
5428 #[test]
5433 fn test_defaults_do_not_include_spawn_background() {
5434 let registry = crate::ToolRegistry::with_defaults();
5435 assert!(
5436 !registry.has("spawn_background"),
5437 "with_defaults() must not include 'spawn_background' — it comes \
5438 from the background_execution capability (EVE-501)"
5439 );
5440 }
5441
5442 #[test]
5447 fn test_capability_features_default_empty() {
5448 let registry = CapabilityRegistry::with_builtins();
5449
5450 let noop = registry.get("noop").unwrap();
5452 assert!(noop.features().is_empty());
5453
5454 let current_time = registry.get("current_time").unwrap();
5455 assert!(current_time.features().is_empty());
5456 }
5457
5458 #[test]
5459 fn test_file_system_capability_features() {
5460 let registry = CapabilityRegistry::with_builtins();
5461
5462 let fs = registry.get("session_file_system").unwrap();
5463 assert_eq!(fs.features(), vec!["file_system"]);
5464 }
5465
5466 #[test]
5467 fn test_bashkit_shell_capability_features() {
5468 let registry = CapabilityRegistry::with_builtins();
5469
5470 let bash = registry.get("bashkit_shell").unwrap();
5471 assert_eq!(bash.features(), vec!["file_system"]);
5472 }
5473
5474 #[test]
5475 fn test_alias_resolves_to_canonical_capability() {
5476 let registry = CapabilityRegistry::with_builtins();
5477
5478 let via_alias = registry.get("virtual_bash").unwrap();
5480 assert_eq!(via_alias.id(), "bashkit_shell");
5481 assert!(registry.has("virtual_bash"));
5482 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5483 assert_eq!(
5484 registry.canonical_id("bashkit_shell"),
5485 Some("bashkit_shell")
5486 );
5487 assert_eq!(registry.canonical_id("nonexistent"), None);
5488 }
5489
5490 #[test]
5491 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5492 let registry = CapabilityRegistry::with_builtins();
5493
5494 let resolved = resolve_dependencies(
5497 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5498 ®istry,
5499 )
5500 .unwrap();
5501 let bash_ids: Vec<_> = resolved
5502 .resolved_ids
5503 .iter()
5504 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5505 .collect();
5506 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5507 assert!(
5509 !resolved
5510 .added_as_dependencies
5511 .contains(&"bashkit_shell".to_string())
5512 );
5513 }
5514
5515 #[test]
5516 fn test_alias_preserves_explicit_config_in_resolution() {
5517 let registry = CapabilityRegistry::with_builtins();
5518
5519 let configs = vec![AgentCapabilityConfig::with_config(
5520 "virtual_bash".to_string(),
5521 serde_json::json!({"key": "value"}),
5522 )];
5523 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5524 let bash = resolved
5525 .iter()
5526 .find(|c| c.capability_id() == "bashkit_shell")
5527 .expect("alias must resolve to canonical bashkit_shell config");
5528 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5529 }
5530
5531 #[test]
5532 fn test_unregister_by_alias_removes_capability_and_aliases() {
5533 let mut registry = CapabilityRegistry::with_builtins();
5534
5535 assert!(registry.unregister("virtual_bash").is_some());
5536 assert!(!registry.has("bashkit_shell"));
5537 assert!(!registry.has("virtual_bash"));
5538 }
5539
5540 #[test]
5541 fn test_session_storage_capability_features() {
5542 let registry = CapabilityRegistry::with_builtins();
5543
5544 let storage = registry.get("session_storage").unwrap();
5545 let features = storage.features();
5546 assert!(features.contains(&"secrets"));
5547 assert!(features.contains(&"key_value"));
5548 }
5549
5550 #[test]
5551 fn test_session_schedule_capability_features() {
5552 let registry = CapabilityRegistry::with_builtins();
5553
5554 let schedule = registry.get("session_schedule").unwrap();
5555 assert_eq!(schedule.features(), vec!["schedules"]);
5556 }
5557
5558 #[test]
5559 fn test_session_sql_database_capability_features() {
5560 let registry = CapabilityRegistry::with_builtins();
5561
5562 let sql = registry.get("session_sql_database").unwrap();
5563 assert_eq!(sql.features(), vec!["sql_database"]);
5564 }
5565
5566 #[test]
5567 fn test_sample_data_capability_features() {
5568 let registry = CapabilityRegistry::with_builtins();
5569
5570 let sample = registry.get("sample_data").unwrap();
5571 assert_eq!(sample.features(), vec!["file_system"]);
5572 }
5573
5574 #[test]
5575 fn test_compute_features_empty() {
5576 let registry = CapabilityRegistry::with_builtins();
5577
5578 let features = compute_features(&[], ®istry);
5579 assert!(features.is_empty());
5580 }
5581
5582 #[test]
5583 fn test_compute_features_single_capability() {
5584 let registry = CapabilityRegistry::with_builtins();
5585
5586 let features = compute_features(&["session_schedule".to_string()], ®istry);
5587 assert_eq!(features, vec!["schedules"]);
5588 }
5589
5590 #[test]
5591 fn test_compute_features_multiple_capabilities() {
5592 let registry = CapabilityRegistry::with_builtins();
5593
5594 let features = compute_features(
5595 &[
5596 "session_file_system".to_string(),
5597 "session_storage".to_string(),
5598 "session_schedule".to_string(),
5599 ],
5600 ®istry,
5601 );
5602 assert!(features.contains(&"file_system".to_string()));
5603 assert!(features.contains(&"secrets".to_string()));
5604 assert!(features.contains(&"key_value".to_string()));
5605 assert!(features.contains(&"schedules".to_string()));
5606 }
5607
5608 #[test]
5609 fn test_compute_features_deduplicates() {
5610 let registry = CapabilityRegistry::with_builtins();
5611
5612 let features = compute_features(
5614 &[
5615 "session_file_system".to_string(),
5616 "bashkit_shell".to_string(),
5617 ],
5618 ®istry,
5619 );
5620 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5621 assert_eq!(file_system_count, 1, "file_system should appear only once");
5622 }
5623
5624 #[test]
5625 fn test_compute_features_includes_dependency_features() {
5626 let registry = CapabilityRegistry::with_builtins();
5627
5628 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5630 assert!(features.contains(&"file_system".to_string()));
5631 }
5632
5633 #[test]
5634 fn test_compute_features_generic_harness_set() {
5635 let registry = CapabilityRegistry::with_builtins();
5636
5637 let features = compute_features(
5639 &[
5640 "session_file_system".to_string(),
5641 "bashkit_shell".to_string(),
5642 "session_storage".to_string(),
5643 "session".to_string(),
5644 "session_schedule".to_string(),
5645 ],
5646 ®istry,
5647 );
5648 assert!(features.contains(&"file_system".to_string()));
5649 assert!(features.contains(&"secrets".to_string()));
5650 assert!(features.contains(&"key_value".to_string()));
5651 assert!(features.contains(&"schedules".to_string()));
5652 }
5653
5654 #[test]
5655 fn test_compute_features_unknown_capability_ignored() {
5656 let registry = CapabilityRegistry::with_builtins();
5657
5658 let features = compute_features(
5659 &["unknown_cap".to_string(), "session_schedule".to_string()],
5660 ®istry,
5661 );
5662 assert_eq!(features, vec!["schedules"]);
5663 }
5664
5665 #[test]
5666 fn test_risk_level_ordering() {
5667 assert!(RiskLevel::Low < RiskLevel::Medium);
5668 assert!(RiskLevel::Medium < RiskLevel::High);
5669 }
5670
5671 #[test]
5672 fn test_risk_level_serde_roundtrip() {
5673 let high = RiskLevel::High;
5674 let json = serde_json::to_string(&high).unwrap();
5675 assert_eq!(json, "\"high\"");
5676 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5677 assert_eq!(back, RiskLevel::High);
5678 }
5679
5680 #[test]
5681 fn test_capability_risk_levels() {
5682 let registry = CapabilityRegistry::with_builtins();
5683
5684 let bash = registry.get("bashkit_shell").unwrap();
5686 assert_eq!(bash.risk_level(), RiskLevel::High);
5687
5688 let fetch = registry.get("web_fetch").unwrap();
5690 assert_eq!(fetch.risk_level(), RiskLevel::High);
5691
5692 let noop = registry.get("noop").unwrap();
5694 assert_eq!(noop.risk_level(), RiskLevel::Low);
5695 }
5696
5697 #[tokio::test]
5702 async fn test_apply_capabilities_openai_tool_search() {
5703 let registry = CapabilityRegistry::with_builtins();
5704 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5705
5706 let applied = apply_capabilities(
5707 base_runtime_agent.clone(),
5708 &["openai_tool_search".to_string()],
5709 ®istry,
5710 &test_ctx(),
5711 )
5712 .await;
5713
5714 assert_eq!(
5716 applied.runtime_agent.system_prompt,
5717 base_runtime_agent.system_prompt
5718 );
5719 assert!(applied.tool_registry.is_empty());
5720 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
5721
5722 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5724 assert!(ts.enabled);
5725 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5726 }
5727
5728 #[tokio::test]
5729 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
5730 let registry = CapabilityRegistry::with_builtins();
5731 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5732
5733 let applied = apply_capabilities(
5734 base_runtime_agent,
5735 &[
5736 "current_time".to_string(),
5737 "openai_tool_search".to_string(),
5738 "test_math".to_string(),
5739 ],
5740 ®istry,
5741 &test_ctx(),
5742 )
5743 .await;
5744
5745 assert!(applied.tool_registry.has("get_current_time"));
5747 assert!(applied.tool_registry.has("add"));
5748 assert!(applied.tool_registry.has("subtract"));
5749 assert!(applied.tool_registry.has("multiply"));
5750 assert!(applied.tool_registry.has("divide"));
5751
5752 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5754 assert!(ts.enabled);
5755 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5756 }
5757
5758 #[tokio::test]
5759 async fn test_collect_capabilities_tool_search_custom_threshold() {
5760 let registry = CapabilityRegistry::with_builtins();
5761
5762 let configs = vec![AgentCapabilityConfig {
5763 capability_ref: CapabilityId::new("openai_tool_search"),
5764 config: serde_json::json!({"threshold": 5}),
5765 }];
5766
5767 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5768
5769 let ts = collected.tool_search.as_ref().unwrap();
5770 assert!(ts.enabled);
5771 assert_eq!(ts.threshold, 5);
5772 }
5773
5774 #[tokio::test]
5775 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
5776 let registry = CapabilityRegistry::with_builtins();
5777
5778 let configs = vec![
5779 AgentCapabilityConfig {
5780 capability_ref: CapabilityId::new("auto_tool_search"),
5781 config: serde_json::json!({"threshold": 2}),
5782 },
5783 AgentCapabilityConfig {
5784 capability_ref: CapabilityId::new("test_math"),
5785 config: serde_json::json!({}),
5786 },
5787 ];
5788
5789 let ctx = test_ctx().with_model("claude-3-5-haiku");
5793 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5794
5795 assert!(
5796 collected.tool_search.is_none(),
5797 "auto_tool_search must not set a hosted config on a non-native model"
5798 );
5799 assert!(
5800 collected
5801 .tools
5802 .iter()
5803 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5804 "auto_tool_search must contribute the client-side tool_search tool"
5805 );
5806 assert!(
5807 !collected.tool_definition_hooks.is_empty(),
5808 "auto_tool_search must contribute a client-side deferral hook"
5809 );
5810
5811 let mut transformed = collected.tool_definitions.clone();
5812 for hook in &collected.tool_definition_hooks {
5813 transformed = hook.transform(transformed);
5814 }
5815 let add_tool = transformed
5816 .iter()
5817 .find(|tool| tool.name() == "add")
5818 .expect("test_math contributes add");
5819 assert!(
5820 add_tool.parameters().get("properties").is_none(),
5821 "generic auto_tool_search must honor the configured threshold"
5822 );
5823 }
5824
5825 #[tokio::test]
5826 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
5827 let registry = CapabilityRegistry::with_builtins();
5828
5829 let configs = vec![AgentCapabilityConfig {
5830 capability_ref: CapabilityId::new("auto_tool_search"),
5831 config: serde_json::json!({"threshold": 7}),
5832 }];
5833
5834 let ctx = test_ctx().with_model("gpt-5.4");
5837 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5838
5839 let ts = collected
5840 .tool_search
5841 .as_ref()
5842 .expect("auto_tool_search must set a hosted config on a native model");
5843 assert!(ts.enabled);
5844 assert_eq!(ts.threshold, 7);
5845 assert!(
5846 !collected
5847 .tools
5848 .iter()
5849 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5850 "hosted mechanism must not contribute the client-side tool_search tool"
5851 );
5852 assert!(
5853 collected.tool_definition_hooks.is_empty(),
5854 "hosted mechanism must not contribute a client-side deferral hook"
5855 );
5856 }
5857
5858 #[tokio::test]
5859 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
5860 let registry = CapabilityRegistry::with_builtins();
5861
5862 let configs = vec![AgentCapabilityConfig {
5863 capability_ref: CapabilityId::new("auto_tool_search"),
5864 config: serde_json::json!({"threshold": 9}),
5865 }];
5866
5867 let ctx = test_ctx().with_model("claude-opus-4-8");
5870 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5871
5872 let ts = collected
5873 .tool_search
5874 .as_ref()
5875 .expect("auto_tool_search must set a hosted config on a native Claude model");
5876 assert!(ts.enabled);
5877 assert_eq!(ts.threshold, 9);
5878 assert!(
5879 !collected
5880 .tools
5881 .iter()
5882 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5883 "hosted mechanism must not contribute the client-side tool_search tool"
5884 );
5885 assert!(
5886 collected.tool_definition_hooks.is_empty(),
5887 "hosted mechanism must not contribute a client-side deferral hook"
5888 );
5889 }
5890
5891 #[tokio::test]
5892 async fn test_collect_capabilities_no_tool_search_without_capability() {
5893 let registry = CapabilityRegistry::with_builtins();
5894
5895 let configs = vec![AgentCapabilityConfig {
5896 capability_ref: CapabilityId::new("current_time"),
5897 config: serde_json::json!({}),
5898 }];
5899
5900 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5901
5902 assert!(collected.tool_search.is_none());
5903 }
5904
5905 #[tokio::test]
5906 async fn test_collect_capabilities_tool_search_category_propagation() {
5907 let registry = CapabilityRegistry::with_builtins();
5908
5909 let configs = vec![
5911 AgentCapabilityConfig {
5912 capability_ref: CapabilityId::new("test_math"),
5913 config: serde_json::json!({}),
5914 },
5915 AgentCapabilityConfig {
5916 capability_ref: CapabilityId::new("openai_tool_search"),
5917 config: serde_json::json!({}),
5918 },
5919 ];
5920
5921 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5922
5923 assert!(collected.tool_search.is_some());
5925
5926 for tool_def in &collected.tool_definitions {
5928 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
5930 assert!(
5931 tool_def.category().is_some(),
5932 "Tool {} should have a category from its capability",
5933 tool_def.name()
5934 );
5935 }
5936 }
5937 }
5938
5939 #[tokio::test]
5940 async fn test_apply_capabilities_prompt_caching() {
5941 let registry = CapabilityRegistry::with_builtins();
5942 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5943
5944 let applied = apply_capabilities(
5945 base_runtime_agent.clone(),
5946 &["prompt_caching".to_string()],
5947 ®istry,
5948 &test_ctx(),
5949 )
5950 .await;
5951
5952 assert_eq!(
5953 applied.runtime_agent.system_prompt,
5954 base_runtime_agent.system_prompt
5955 );
5956 assert!(applied.tool_registry.is_empty());
5957 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
5958
5959 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
5960 assert!(prompt_cache.enabled);
5961 assert_eq!(
5962 prompt_cache.strategy,
5963 crate::driver_registry::PromptCacheStrategy::Auto
5964 );
5965 assert!(prompt_cache.gemini_cached_content.is_none());
5966 }
5967
5968 #[tokio::test]
5969 async fn test_apply_capabilities_openrouter_server_tools() {
5970 let registry = CapabilityRegistry::with_builtins();
5971 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5972
5973 let configs = vec![AgentCapabilityConfig {
5974 capability_ref: CapabilityId::new("openrouter_server_tools"),
5975 config: serde_json::json!({
5976 "tools": ["web_search", "datetime"],
5977 "web_search_max_results": 4,
5978 }),
5979 }];
5980
5981 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5982 let routing = collected
5983 .openrouter_routing
5984 .as_ref()
5985 .expect("server tools produce routing config");
5986 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
5987 assert_eq!(
5988 kinds,
5989 vec![
5990 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
5991 crate::driver_registry::OpenRouterServerToolKind::Datetime,
5992 ]
5993 );
5994
5995 let applied = apply_capabilities(
5998 base_runtime_agent,
5999 &["openrouter_server_tools".to_string()],
6000 ®istry,
6001 &test_ctx(),
6002 )
6003 .await;
6004 assert!(applied.tool_registry.is_empty());
6005 assert!(applied.runtime_agent.openrouter_routing.is_none());
6006 }
6007
6008 #[tokio::test]
6009 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6010 let registry = CapabilityRegistry::with_builtins();
6011
6012 let configs = vec![AgentCapabilityConfig {
6013 capability_ref: CapabilityId::new("prompt_caching"),
6014 config: serde_json::json!({"strategy": "auto"}),
6015 }];
6016
6017 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6018
6019 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6020 assert!(prompt_cache.enabled);
6021 assert_eq!(
6022 prompt_cache.strategy,
6023 crate::driver_registry::PromptCacheStrategy::Auto
6024 );
6025 assert!(prompt_cache.gemini_cached_content.is_none());
6026 }
6027
6028 #[tokio::test]
6029 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6030 let registry = CapabilityRegistry::with_builtins();
6031
6032 let configs = vec![AgentCapabilityConfig {
6033 capability_ref: CapabilityId::new("prompt_caching"),
6034 config: serde_json::json!({
6035 "strategy": "auto",
6036 "gemini_cached_content": "cachedContents/demo-cache"
6037 }),
6038 }];
6039
6040 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6041
6042 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6043 assert_eq!(
6044 prompt_cache.gemini_cached_content.as_deref(),
6045 Some("cachedContents/demo-cache")
6046 );
6047 }
6048
6049 #[tokio::test]
6050 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6051 let registry = CapabilityRegistry::with_builtins();
6052
6053 let collected = collect_capabilities_with_configs(
6055 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6056 ®istry,
6057 &test_ctx(),
6058 )
6059 .await;
6060 assert_eq!(collected.parallel_tool_calls, Some(true));
6061
6062 let collected = collect_capabilities_with_configs(
6064 &[AgentCapabilityConfig {
6065 capability_ref: CapabilityId::new("parallel_tool_calls"),
6066 config: serde_json::json!({"mode": "avoid"}),
6067 }],
6068 ®istry,
6069 &test_ctx(),
6070 )
6071 .await;
6072 assert_eq!(collected.parallel_tool_calls, Some(false));
6073
6074 let collected = collect_capabilities_with_configs(
6076 &[AgentCapabilityConfig {
6077 capability_ref: CapabilityId::new("parallel_tool_calls"),
6078 config: serde_json::json!({"mode": "none"}),
6079 }],
6080 ®istry,
6081 &test_ctx(),
6082 )
6083 .await;
6084 assert_eq!(collected.parallel_tool_calls, None);
6085
6086 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6088 assert_eq!(collected.parallel_tool_calls, None);
6089 }
6090
6091 #[tokio::test]
6092 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6093 let registry = CapabilityRegistry::with_builtins();
6094
6095 let applied = apply_capabilities(
6097 RuntimeAgent::new("p", "gpt-5.2"),
6098 &["parallel_tool_calls".to_string()],
6099 ®istry,
6100 &test_ctx(),
6101 )
6102 .await;
6103 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6104
6105 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6107 base.parallel_tool_calls = Some(false);
6108 let applied = apply_capabilities(
6109 base,
6110 &["parallel_tool_calls".to_string()],
6111 ®istry,
6112 &test_ctx(),
6113 )
6114 .await;
6115 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6116 }
6117
6118 struct SkillContributingCapability;
6123
6124 impl Capability for SkillContributingCapability {
6125 fn id(&self) -> &str {
6126 "contributes_skills"
6127 }
6128 fn name(&self) -> &str {
6129 "Contributes Skills"
6130 }
6131 fn description(&self) -> &str {
6132 "Test capability that contributes skills."
6133 }
6134 fn contribute_skills(&self) -> Vec<SkillContribution> {
6135 vec![
6136 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6137 .with_files(vec![(
6138 "scripts/a.sh".to_string(),
6139 "#!/bin/sh\necho a\n".to_string(),
6140 )]),
6141 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6142 .with_user_invocable(false),
6143 ]
6144 }
6145 }
6146
6147 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6148 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6149 MountSource::InlineFile { content, .. } => content.as_str(),
6150 _ => panic!("Expected InlineFile for SKILL.md"),
6151 }
6152 }
6153
6154 #[tokio::test]
6155 async fn test_contribute_skills_normalized_to_mounts() {
6156 let mut registry = CapabilityRegistry::new();
6157 registry.register(SkillContributingCapability);
6158
6159 let configs = vec![AgentCapabilityConfig {
6160 capability_ref: CapabilityId::new("contributes_skills"),
6161 config: serde_json::json!({}),
6162 }];
6163
6164 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6165
6166 let skill_mounts: Vec<_> = collected
6167 .mounts
6168 .iter()
6169 .filter(|m| m.path.starts_with("/.agents/skills/"))
6170 .collect();
6171 assert_eq!(skill_mounts.len(), 2);
6172
6173 for m in &skill_mounts {
6176 assert!(m.is_readonly());
6177 assert_eq!(m.capability_id, "contributes_skills");
6178 }
6179
6180 let alpha = skill_mounts
6181 .iter()
6182 .find(|m| m.path == "/.agents/skills/alpha-skill")
6183 .expect("alpha-skill mount missing");
6184 match &alpha.source {
6185 MountSource::InlineDirectory { entries } => {
6186 assert!(entries.contains_key("SKILL.md"));
6187 assert!(entries.contains_key("scripts/a.sh"));
6188 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6189 assert_eq!(parsed.name, "alpha-skill");
6190 assert!(parsed.user_invocable);
6191 }
6192 _ => panic!("Expected InlineDirectory"),
6193 }
6194
6195 let beta = skill_mounts
6196 .iter()
6197 .find(|m| m.path == "/.agents/skills/beta-skill")
6198 .expect("beta-skill mount missing");
6199 match &beta.source {
6200 MountSource::InlineDirectory { entries } => {
6201 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6202 assert!(!parsed.user_invocable);
6203 }
6204 _ => panic!("Expected InlineDirectory"),
6205 }
6206 }
6207
6208 #[tokio::test]
6209 async fn test_contribute_skills_default_empty() {
6210 let mut registry = CapabilityRegistry::new();
6213 registry.register(FilterTestCapability { priority: 0 });
6214
6215 let configs = vec![AgentCapabilityConfig {
6216 capability_ref: CapabilityId::new("filter_test"),
6217 config: serde_json::json!({}),
6218 }];
6219
6220 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6221 assert!(
6222 collected
6223 .mounts
6224 .iter()
6225 .all(|m| !m.path.starts_with("/.agents/skills/"))
6226 );
6227 }
6228
6229 struct LocalizedCapability;
6230
6231 impl Capability for LocalizedCapability {
6232 fn id(&self) -> &str {
6233 "localized"
6234 }
6235 fn name(&self) -> &str {
6236 "Localized"
6237 }
6238 fn description(&self) -> &str {
6239 "English description"
6240 }
6241 fn localizations(&self) -> Vec<CapabilityLocalization> {
6242 vec![
6243 CapabilityLocalization {
6244 locale: "en",
6245 name: None,
6246 description: None,
6247 config_description: Some("Controls things."),
6248 config_overlay: None,
6249 },
6250 CapabilityLocalization {
6251 locale: "uk",
6252 name: Some("Локалізована"),
6253 description: Some("Український опис"),
6254 config_description: Some("Керує налаштуваннями."),
6255 config_overlay: None,
6256 },
6257 ]
6258 }
6259 }
6260
6261 #[test]
6262 fn localized_name_falls_back_exact_language_then_base() {
6263 let cap = LocalizedCapability;
6264 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6266 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6267 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6269 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6271 assert_eq!(cap.localized_name(None), "Localized");
6272 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6273 assert_eq!(cap.localized_description(Some("de")), "English description");
6274 }
6275
6276 #[test]
6277 fn describe_schema_resolves_config_description_per_locale() {
6278 let cap = LocalizedCapability;
6279 assert_eq!(
6280 cap.describe_schema(Some("uk-UA")).as_deref(),
6281 Some("Керує налаштуваннями.")
6282 );
6283 assert_eq!(
6285 cap.describe_schema(Some("pl")).as_deref(),
6286 Some("Controls things.")
6287 );
6288 assert_eq!(
6289 cap.describe_schema(None).as_deref(),
6290 Some("Controls things.")
6291 );
6292 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6294 }
6295}