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,
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 ReportProgressTool, ReportResultTool, SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool,
328 SubagentCapability, report_progress_tool_for_child_session,
329 report_result_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 "target": {
1854 "type": "object",
1855 "properties": {
1856 "type": {
1857 "type": "string",
1858 "enum": self.target_types(),
1859 "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."
1860 },
1861 "id": {
1862 "type": "string",
1863 "description": "Configured first-party handoff target id. Also accepted as an alias for external_a2a target.external_agent_id."
1864 },
1865 "external_agent_id": {
1866 "type": "string",
1867 "description": "Configured external A2A agent id."
1868 }
1869 },
1870 "required": ["type"],
1871 "additionalProperties": false
1872 },
1873 "mode": {
1874 "type": "string",
1875 "enum": ["background", "foreground", "wait"],
1876 "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."
1877 },
1878 "blueprint": {
1879 "type": "string",
1880 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
1881 },
1882 "config": {
1883 "type": "object",
1884 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
1885 },
1886 "result_schema": {
1887 "type": "object",
1888 "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."
1889 },
1890 "message_schema": {
1891 "type": "object",
1892 "description": "Subagent-only JSON Schema for structured progress messages. When set, the child receives report_progress and valid calls post data messages to the task thread."
1893 },
1894 "public_context": {
1895 "type": "object",
1896 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
1897 },
1898 "wait_timeout_secs": {
1899 "type": "integer",
1900 "minimum": 1,
1901 "maximum": 86400,
1902 "description": "External-A2A-only foreground/wait timeout."
1903 },
1904 "wake_on_completion": {
1905 "type": "boolean",
1906 "description": "External-A2A-only control for background completion wake-ups."
1907 }
1908 },
1909 "required": ["instructions", "target"],
1910 "additionalProperties": false
1911 })
1912 }
1913
1914 fn hints(&self) -> crate::tool_types::ToolHints {
1915 let mut hints = crate::tool_types::ToolHints::default().with_long_running(true);
1916 if self.provider_for("external_a2a").is_some() {
1917 hints = hints.with_open_world(true);
1918 }
1919 hints
1920 }
1921
1922 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
1923 ToolExecutionResult::tool_error(
1924 "spawn_agent requires context. This tool must be executed with session context.",
1925 )
1926 }
1927
1928 async fn execute_with_context(
1929 &self,
1930 arguments: serde_json::Value,
1931 context: &ToolContext,
1932 ) -> ToolExecutionResult {
1933 let target_type = match arguments
1934 .get("target")
1935 .and_then(|target| target.get("type"))
1936 .and_then(serde_json::Value::as_str)
1937 {
1938 Some(target_type) => target_type,
1939 None => {
1940 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
1941 }
1942 };
1943
1944 let Some(provider) = self.provider_for(target_type) else {
1945 let supported = self.target_types().join(", ");
1946 return ToolExecutionResult::tool_error(format!(
1947 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
1948 ));
1949 };
1950
1951 provider
1952 .execute_with_context(self.normalize_arguments(arguments), context)
1953 .await
1954 }
1955
1956 fn requires_context(&self) -> bool {
1957 true
1958 }
1959}
1960
1961pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
1966 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
1967 return base_system_prompt.to_string();
1968 };
1969
1970 if base_system_prompt.is_empty() {
1971 return additions.to_string();
1972 }
1973
1974 if base_system_prompt.contains("<system-prompt>") {
1975 format!("{base_system_prompt}\n\n{additions}")
1976 } else {
1977 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
1978 }
1979}
1980
1981pub struct CollectedMessageFilters {
1988 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1990}
1991
1992pub struct CollectedModelViewProviders {
1994 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1996}
1997
1998impl CollectedMessageFilters {
2004 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2006 for (provider, config) in &self.message_filter_providers {
2007 provider.apply_filters(query, config);
2008 }
2009 }
2010
2011 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2013 for (provider, config) in &self.message_filter_providers {
2014 provider.post_load(messages, config);
2015 }
2016 }
2017}
2018
2019impl CollectedModelViewProviders {
2020 pub fn apply_model_view(
2022 &self,
2023 mut messages: Vec<Message>,
2024 context: &ModelViewContext<'_>,
2025 ) -> Vec<Message> {
2026 for (provider, config) in &self.model_view_providers {
2027 messages = provider.apply_model_view(messages, config, context);
2028 }
2029 messages
2030 }
2031}
2032
2033fn compaction_is_enabled(
2039 capability_configs: &[AgentCapabilityConfig],
2040 registry: &CapabilityRegistry,
2041) -> bool {
2042 capability_configs.iter().any(|cap_config| {
2043 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2044 && registry
2045 .get(cap_config.capability_ref.as_str())
2046 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2047 })
2048}
2049
2050fn message_filter_config_for(
2059 cap_id: &str,
2060 base: &serde_json::Value,
2061 compaction_on: bool,
2062) -> serde_json::Value {
2063 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2064 return base.clone();
2065 }
2066 let mut config = base.clone();
2067 match config.as_object_mut() {
2068 Some(map) => {
2069 map.insert(
2070 "compaction_active".to_string(),
2071 serde_json::Value::Bool(true),
2072 );
2073 }
2074 None => {
2075 config = serde_json::json!({ "compaction_active": true });
2076 }
2077 }
2078 config
2079}
2080
2081pub fn collect_message_filters_only(
2087 capability_configs: &[AgentCapabilityConfig],
2088 registry: &CapabilityRegistry,
2089) -> CollectedMessageFilters {
2090 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2091 Vec::new();
2092 let compaction_on = compaction_is_enabled(capability_configs, registry);
2093
2094 for cap_config in capability_configs {
2095 let cap_id = cap_config.capability_ref.as_str();
2096 if let Some(capability) = registry.get(cap_id) {
2097 if capability.status() != CapabilityStatus::Available {
2098 continue;
2099 }
2100 let effective: &dyn Capability = capability
2103 .resolve_for_model(None)
2104 .unwrap_or_else(|| capability.as_ref());
2105 if let Some(provider) = effective.message_filter_provider() {
2106 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2107 message_filter_providers.push((provider, config));
2108 }
2109 }
2110 }
2111
2112 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2113
2114 CollectedMessageFilters {
2115 message_filter_providers,
2116 }
2117}
2118
2119pub fn collect_model_view_providers(
2126 capability_configs: &[AgentCapabilityConfig],
2127 registry: &CapabilityRegistry,
2128 model: Option<&str>,
2129) -> CollectedModelViewProviders {
2130 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2131
2132 for cap_config in capability_configs {
2133 let cap_id = cap_config.capability_ref.as_str();
2134 if let Some(capability) = registry.get(cap_id) {
2135 if capability.status() != CapabilityStatus::Available {
2136 continue;
2137 }
2138 let effective: &dyn Capability = capability
2139 .resolve_for_model(model)
2140 .unwrap_or_else(|| capability.as_ref());
2141 if let Some(provider) = effective.model_view_provider() {
2142 model_view_providers.push((provider, cap_config.config.clone()));
2143 }
2144 }
2145 }
2146
2147 model_view_providers.sort_by_key(|(p, _)| p.priority());
2148
2149 CollectedModelViewProviders {
2150 model_view_providers,
2151 }
2152}
2153
2154pub fn collect_dynamic_facts(
2160 capability_configs: &[AgentCapabilityConfig],
2161 registry: &CapabilityRegistry,
2162 model: Option<&str>,
2163 ctx: &FactsContext,
2164) -> Vec<Fact> {
2165 let mut dynamic = Vec::new();
2166 for cap_config in capability_configs {
2167 let cap_id = cap_config.capability_ref.as_str();
2168 if let Some(capability) = registry.get(cap_id) {
2169 if capability.status() != CapabilityStatus::Available {
2170 continue;
2171 }
2172 let effective: &dyn Capability = capability
2173 .resolve_for_model(model)
2174 .unwrap_or_else(|| capability.as_ref());
2175 for fact in effective.facts(&cap_config.config, ctx) {
2176 if fact.volatility == Volatility::Dynamic {
2177 dynamic.push(fact);
2178 }
2179 }
2180 }
2181 }
2182 dynamic
2183}
2184
2185pub fn collect_capability_mcp_servers(
2186 capability_configs: &[AgentCapabilityConfig],
2187 registry: &CapabilityRegistry,
2188) -> ScopedMcpServers {
2189 let mut servers = ScopedMcpServers::default();
2190
2191 for cap_config in capability_configs {
2192 let cap_id = cap_config.capability_ref.as_str();
2193 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2196 if let Ok(definition) =
2197 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2198 {
2199 if definition.status != CapabilityStatus::Available {
2200 continue;
2201 }
2202 if let Some(contributed) = definition.mcp_servers {
2203 servers = merge_scoped_mcp_servers(&servers, &contributed);
2204 }
2205 }
2206 continue;
2207 }
2208 if let Some(capability) = registry.get(cap_id) {
2209 if capability.status() != CapabilityStatus::Available {
2210 continue;
2211 }
2212 servers = merge_scoped_mcp_servers(
2213 &servers,
2214 &capability.mcp_servers_with_config(&cap_config.config),
2215 );
2216 }
2217 }
2218
2219 servers
2220}
2221
2222pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2229
2230#[derive(Debug, Clone, PartialEq, Eq)]
2232pub enum DependencyError {
2233 CircularDependency {
2235 capability_id: String,
2237 chain: Vec<String>,
2239 },
2240 TooManyCapabilities {
2242 count: usize,
2244 max: usize,
2246 },
2247}
2248
2249impl std::fmt::Display for DependencyError {
2250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2251 match self {
2252 DependencyError::CircularDependency {
2253 capability_id,
2254 chain,
2255 } => {
2256 write!(
2257 f,
2258 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2259 capability_id,
2260 chain.join(" -> "),
2261 capability_id
2262 )
2263 }
2264 DependencyError::TooManyCapabilities { count, max } => {
2265 write!(
2266 f,
2267 "Too many capabilities after resolution: {} (max: {})",
2268 count, max
2269 )
2270 }
2271 }
2272 }
2273}
2274
2275impl std::error::Error for DependencyError {}
2276
2277#[derive(Debug, Clone)]
2279pub struct ResolvedCapabilities {
2280 pub resolved_ids: Vec<String>,
2283 pub added_as_dependencies: Vec<String>,
2285 pub user_selected: Vec<String>,
2287}
2288
2289pub fn resolve_dependencies(
2309 selected_ids: &[String],
2310 registry: &CapabilityRegistry,
2311) -> Result<ResolvedCapabilities, DependencyError> {
2312 use std::collections::HashSet;
2313
2314 let user_selected: HashSet<String> = selected_ids
2316 .iter()
2317 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2318 .collect();
2319 let mut resolved: Vec<String> = Vec::new();
2320 let mut resolved_set: HashSet<String> = HashSet::new();
2321 let mut added_as_dependencies: Vec<String> = Vec::new();
2322
2323 for cap_id in selected_ids {
2325 resolve_single_capability(
2326 cap_id,
2327 registry,
2328 &mut resolved,
2329 &mut resolved_set,
2330 &mut added_as_dependencies,
2331 &user_selected,
2332 &mut Vec::new(), )?;
2334 }
2335
2336 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2338 return Err(DependencyError::TooManyCapabilities {
2339 count: resolved.len(),
2340 max: MAX_RESOLVED_CAPABILITIES,
2341 });
2342 }
2343
2344 Ok(ResolvedCapabilities {
2345 resolved_ids: resolved,
2346 added_as_dependencies,
2347 user_selected: selected_ids.to_vec(),
2348 })
2349}
2350
2351pub fn resolve_capability_configs(
2356 selected_configs: &[AgentCapabilityConfig],
2357 registry: &CapabilityRegistry,
2358) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2359 let mut selected_ids: Vec<String> = Vec::new();
2360 for config in selected_configs {
2361 if (is_declarative_capability(config.capability_id())
2364 || is_plugin_capability(config.capability_id()))
2365 && let Ok(definition) =
2366 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2367 {
2368 selected_ids.extend(definition.dependencies);
2369 }
2370 selected_ids.push(config.capability_id().to_string());
2371 }
2372 let resolved = resolve_dependencies(&selected_ids, registry)?;
2373
2374 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2377 .iter()
2378 .map(|config| {
2379 let id = config.capability_id();
2380 let id = registry.canonical_id(id).unwrap_or(id);
2381 (id.to_string(), config.config.clone())
2382 })
2383 .collect();
2384
2385 Ok(resolved
2386 .resolved_ids
2387 .into_iter()
2388 .map(|capability_id| {
2389 explicit_configs
2390 .get(&capability_id)
2391 .cloned()
2392 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2393 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2394 })
2395 .collect())
2396}
2397
2398fn resolve_single_capability(
2400 cap_id: &str,
2401 registry: &CapabilityRegistry,
2402 resolved: &mut Vec<String>,
2403 resolved_set: &mut std::collections::HashSet<String>,
2404 added_as_dependencies: &mut Vec<String>,
2405 user_selected: &std::collections::HashSet<String>,
2406 visiting: &mut Vec<String>,
2407) -> Result<(), DependencyError> {
2408 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2412
2413 if resolved_set.contains(cap_id) {
2415 return Ok(());
2416 }
2417
2418 if visiting.contains(&cap_id.to_string()) {
2420 return Err(DependencyError::CircularDependency {
2421 capability_id: cap_id.to_string(),
2422 chain: visiting.clone(),
2423 });
2424 }
2425
2426 let capability = match registry.get(cap_id) {
2428 Some(cap) => cap,
2429 None => {
2430 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2434 && !resolved_set.contains(cap_id)
2435 {
2436 resolved.push(cap_id.to_string());
2437 resolved_set.insert(cap_id.to_string());
2438 if !user_selected.contains(cap_id) {
2439 added_as_dependencies.push(cap_id.to_string());
2440 }
2441 }
2442 return Ok(());
2443 }
2444 };
2445
2446 visiting.push(cap_id.to_string());
2448
2449 for dep_id in capability.dependencies() {
2451 resolve_single_capability(
2452 dep_id,
2453 registry,
2454 resolved,
2455 resolved_set,
2456 added_as_dependencies,
2457 user_selected,
2458 visiting,
2459 )?;
2460 }
2461
2462 visiting.pop();
2464
2465 if !resolved_set.contains(cap_id) {
2467 resolved.push(cap_id.to_string());
2468 resolved_set.insert(cap_id.to_string());
2469
2470 if !user_selected.contains(cap_id) {
2472 added_as_dependencies.push(cap_id.to_string());
2473 }
2474 }
2475
2476 Ok(())
2477}
2478
2479pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2484 use std::collections::HashSet;
2485
2486 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2487 Ok(resolved) => resolved.resolved_ids,
2488 Err(_) => capability_ids.to_vec(),
2489 };
2490
2491 let mut seen = HashSet::new();
2492 let mut features = Vec::new();
2493 for cap_id in &resolved_ids {
2494 if let Some(cap) = registry.get(cap_id) {
2495 for feature in cap.features() {
2496 if seen.insert(feature) {
2497 features.push(feature.to_string());
2498 }
2499 }
2500 }
2501 }
2502 features
2503}
2504
2505pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2508 registry
2509 .get(cap_id)
2510 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2511 .unwrap_or_default()
2512}
2513
2514pub async fn collect_capabilities(
2530 capability_ids: &[String],
2531 registry: &CapabilityRegistry,
2532 ctx: &SystemPromptContext,
2533) -> CollectedCapabilities {
2534 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2537 Ok(resolved) => resolved.resolved_ids,
2538 Err(e) => {
2539 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2540 capability_ids.to_vec()
2541 }
2542 };
2543
2544 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2546 .iter()
2547 .map(|id| AgentCapabilityConfig {
2548 capability_ref: CapabilityId::new(id),
2549 config: serde_json::Value::Object(serde_json::Map::new()),
2550 })
2551 .collect();
2552
2553 collect_capabilities_with_configs(&configs, registry, ctx).await
2554}
2555
2556pub async fn collect_capabilities_with_configs(
2567 capability_configs: &[AgentCapabilityConfig],
2568 registry: &CapabilityRegistry,
2569 ctx: &SystemPromptContext,
2570) -> CollectedCapabilities {
2571 let mut system_prompt_parts: Vec<String> = Vec::new();
2572 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2573 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2574 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2575 let mut mounts: Vec<MountPoint> = Vec::new();
2576 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2577 Vec::new();
2578 let mut applied_ids: Vec<String> = Vec::new();
2579 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2580 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2581 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2582 let mut parallel_tool_calls: Option<bool> = None;
2583 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2584 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2585 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2588 let mut mcp_servers = ScopedMcpServers::default();
2589 let mut static_facts: Vec<Fact> = Vec::new();
2593 let mut has_dynamic_facts = false;
2594 let facts_ctx = FactsContext::new(ctx.session_id);
2595 let compaction_on = compaction_is_enabled(capability_configs, registry);
2596 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2597 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2598
2599 for cap_config in capability_configs {
2600 let cap_id = cap_config.capability_ref.as_str();
2601 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2606 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2607 cap_config.config.clone(),
2608 ) {
2609 Ok(definition) => {
2610 if definition.status != CapabilityStatus::Available {
2611 continue;
2612 }
2613
2614 if let Some(prompt) = definition.system_prompt.as_deref() {
2615 let contribution =
2616 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2617 system_prompt_attributions.push(SystemPromptAttribution {
2618 capability_id: cap_id.to_string(),
2619 content: contribution.clone(),
2620 });
2621 system_prompt_parts.push(contribution);
2622 }
2623
2624 mounts.extend(definition.mounts(cap_id));
2625 if let Some(ref servers) = definition.mcp_servers {
2626 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2627 }
2628 for skill in definition.skill_contributions() {
2629 mounts.push(skill.to_mount(cap_id));
2630 }
2631
2632 applied_ids.push(cap_id.to_string());
2633 }
2634 Err(error) => {
2635 tracing::warn!(
2636 capability_id = %cap_id,
2637 error = %error,
2638 "Skipping invalid declarative/plugin capability config"
2639 );
2640 }
2641 }
2642 continue;
2643 }
2644 if let Some(capability) = registry.get(cap_id) {
2645 if capability.status() != CapabilityStatus::Available {
2647 continue;
2648 }
2649
2650 let effective: &dyn Capability =
2662 match capability.resolve_for_model(ctx.model.as_deref()) {
2663 Some(inner) => inner,
2664 None => capability.as_ref(),
2665 };
2666 let effective_id = effective.id();
2667 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2668 agent_handoff_spawn_config = Some(cap_config.config.clone());
2669 }
2670
2671 if let Some(contribution) = effective
2673 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2674 .await
2675 {
2676 system_prompt_attributions.push(SystemPromptAttribution {
2677 capability_id: cap_id.to_string(),
2678 content: contribution.clone(),
2679 });
2680 system_prompt_parts.push(contribution);
2681 }
2682
2683 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2688 match fact.volatility {
2689 Volatility::Static => static_facts.push(fact),
2690 Volatility::Dynamic => has_dynamic_facts = true,
2691 }
2692 }
2693
2694 for tool in effective.tools_with_config(&cap_config.config) {
2696 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2697 spawn_agent_providers.push(SpawnAgentTargetProvider {
2698 target_type: "external_a2a",
2699 tool,
2700 });
2701 } else {
2702 tools.push(tool);
2703 }
2704 }
2705 tool_definition_hooks
2706 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2707 tool_call_hooks.extend(effective.tool_call_hooks());
2708 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2710 let cap_category = effective.category();
2715 for def in effective.tool_definitions() {
2716 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2717 continue;
2718 }
2719 let def = match (def.category(), cap_category) {
2720 (None, Some(cat)) => def.with_category(cat),
2721 _ => def,
2722 }
2723 .with_capability_attribution(cap_id, Some(capability.name()));
2724 tool_definitions.push(def);
2725 }
2726
2727 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2735 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2736 {
2737 let threshold = cap_config
2739 .config
2740 .get("threshold")
2741 .and_then(|v| v.as_u64())
2742 .map(|v| v as usize)
2743 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2744 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2745 enabled: true,
2746 threshold,
2747 });
2748 }
2749
2750 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2751 let strategy = cap_config
2752 .config
2753 .get("strategy")
2754 .and_then(|v| v.as_str())
2755 .map(|value| match value {
2756 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2757 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2758 })
2759 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2760 let gemini_cached_content = cap_config
2761 .config
2762 .get("gemini_cached_content")
2763 .and_then(|v| v.as_str())
2764 .map(str::to_string);
2765 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2766 enabled: true,
2767 strategy,
2768 gemini_cached_content,
2769 });
2770 }
2771
2772 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2773 parallel_tool_calls =
2774 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2775 }
2776
2777 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2778 let server_tools =
2779 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2780 if !server_tools.is_empty() {
2781 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2782 server_tools,
2783 ..Default::default()
2784 });
2785 }
2786 }
2787
2788 mounts.extend(effective.mounts());
2790
2791 mcp_servers = merge_scoped_mcp_servers(
2792 &mcp_servers,
2793 &effective.mcp_servers_with_config(&cap_config.config),
2794 );
2795
2796 for skill in effective.contribute_skills() {
2800 mounts.push(skill.to_mount(cap_id));
2801 }
2802
2803 if let Some(provider) = effective.message_filter_provider() {
2805 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2806 message_filter_providers.push((provider, config));
2807 }
2808
2809 applied_ids.push(cap_id.to_string());
2810 }
2811 }
2812
2813 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
2818 spawn_agent_providers.push(SpawnAgentTargetProvider {
2819 target_type: "subagent",
2820 tool: Box::new(SpawnSubagentAsAgentTool),
2821 });
2822 }
2823 if let Some(config) = agent_handoff_spawn_config.as_ref() {
2824 spawn_agent_providers.push(SpawnAgentTargetProvider {
2825 target_type: "agent",
2826 tool: Box::new(SpawnAgentHandoffTool::new(config)),
2827 });
2828 }
2829 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
2830 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
2831 let def = tool
2832 .to_definition()
2833 .with_category("Orchestration")
2834 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
2835 tools.push(Box::new(tool));
2836 tool_definitions.push(def);
2837 }
2838
2839 if !applied_ids
2851 .iter()
2852 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2853 && tool_definitions
2854 .iter()
2855 .any(|def| def.hints().supports_background == Some(true))
2856 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2857 && bg_cap.status() == CapabilityStatus::Available
2858 {
2859 tools.extend(bg_cap.tools());
2860 let cap_category = bg_cap.category();
2861 for def in bg_cap.tool_definitions() {
2862 let def = match (def.category(), cap_category) {
2863 (None, Some(cat)) => def.with_category(cat),
2864 _ => def,
2865 }
2866 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2867 tool_definitions.push(def);
2868 }
2869 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2870 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2871 }
2872
2873 if let Some(block) = facts::render_facts_block(&static_facts) {
2878 system_prompt_attributions.push(SystemPromptAttribution {
2879 capability_id: "facts".to_string(),
2880 content: block.clone(),
2881 });
2882 system_prompt_parts.push(block);
2883 }
2884 if has_dynamic_facts {
2885 system_prompt_attributions.push(SystemPromptAttribution {
2886 capability_id: "facts".to_string(),
2887 content: FACTS_DYNAMIC_NOTE.to_string(),
2888 });
2889 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
2890 }
2891
2892 tool_call_hooks.extend(narration_hooks);
2896
2897 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2899
2900 CollectedCapabilities {
2901 system_prompt_parts,
2902 system_prompt_attributions,
2903 tools,
2904 tool_definitions,
2905 mounts,
2906 message_filter_providers,
2907 applied_ids,
2908 tool_search,
2909 prompt_cache,
2910 openrouter_routing,
2911 parallel_tool_calls,
2912 tool_definition_hooks,
2913 tool_call_hooks,
2914 mcp_servers,
2915 }
2916}
2917
2918pub struct AppliedCapabilities {
2924 pub runtime_agent: RuntimeAgent,
2926 pub tool_registry: ToolRegistry,
2928 pub applied_ids: Vec<String>,
2930}
2931
2932pub async fn apply_capabilities(
2969 base_runtime_agent: RuntimeAgent,
2970 capability_ids: &[String],
2971 registry: &CapabilityRegistry,
2972 ctx: &SystemPromptContext,
2973) -> AppliedCapabilities {
2974 let collected = collect_capabilities(capability_ids, registry, ctx).await;
2975
2976 let final_system_prompt = compose_system_prompt(
2978 &base_runtime_agent.system_prompt,
2979 collected.system_prompt_prefix().as_deref(),
2980 );
2981
2982 let mut tool_registry = ToolRegistry::new();
2984 for tool in collected.tools {
2985 tool_registry.register_boxed(tool);
2986 }
2987
2988 let mut tools = collected.tool_definitions;
2990 for hook in &collected.tool_definition_hooks {
2991 tools = hook.transform(tools);
2992 }
2993
2994 let runtime_agent = RuntimeAgent {
2995 system_prompt: final_system_prompt,
2996 model: base_runtime_agent.model,
2997 tools,
2998 max_iterations: base_runtime_agent.max_iterations,
2999 temperature: base_runtime_agent.temperature,
3000 max_tokens: base_runtime_agent.max_tokens,
3001 tool_search: collected.tool_search,
3002 prompt_cache: collected.prompt_cache,
3003 openrouter_routing: collected.openrouter_routing,
3004 network_access: base_runtime_agent.network_access,
3005 parallel_tool_calls: base_runtime_agent
3008 .parallel_tool_calls
3009 .or(collected.parallel_tool_calls),
3010 };
3011
3012 AppliedCapabilities {
3013 runtime_agent,
3014 tool_registry,
3015 applied_ids: collected.applied_ids,
3016 }
3017}
3018
3019#[cfg(test)]
3024mod tests {
3025 use super::*;
3026 use crate::typed_id::SessionId;
3027 use std::collections::BTreeSet;
3028 use uuid::Uuid;
3029
3030 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3032
3033 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3034 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3035 }
3036
3037 fn test_ctx() -> SystemPromptContext {
3039 SystemPromptContext::without_file_store(SessionId::new())
3040 }
3041
3042 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3044 let mut ids = [
3045 "agent_instructions",
3046 "human_intent",
3047 "budgeting",
3048 "self_budget",
3049 "noop",
3050 "current_time",
3051 "research",
3052 "platform_management",
3053 "session_file_system",
3054 "session_storage",
3055 "session",
3056 "session_sql_database",
3057 "test_math",
3058 "test_weather",
3059 "stateless_todo_list",
3060 "web_fetch",
3061 "bashkit_shell",
3062 "background_execution",
3063 "session_schedule",
3064 "btw",
3065 "infinity_context",
3066 "compaction",
3067 "memory",
3068 "message_metadata",
3069 "openai_tool_search",
3070 "claude_tool_search",
3071 "tool_search",
3072 "auto_tool_search",
3073 "prompt_caching",
3074 "parallel_tool_calls",
3075 "session_tasks",
3076 "skills",
3077 "subagents",
3078 "system_commands",
3079 "sample_data",
3080 "data_knowledge",
3081 "knowledge_base",
3082 "knowledge_index",
3083 "tool_output_persistence",
3084 "tool_output_distillation",
3085 "fake_warehouse",
3086 "fake_aws",
3087 "fake_crm",
3088 "fake_financial",
3089 "loop_detection",
3090 "tool_call_repair",
3091 "error_disclosure",
3092 "prompt_canary_guardrail",
3093 "guardrails",
3094 "user_hooks",
3095 "model_scout",
3096 "openrouter_workspace",
3097 "openrouter_server_tools",
3098 ]
3099 .into_iter()
3100 .collect::<BTreeSet<_>>();
3101 if cfg!(feature = "ui-capabilities") {
3102 ids.insert("openui");
3103 ids.insert("a2ui");
3104 }
3105 ids
3106 }
3107
3108 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3110 let mut ids = [
3111 "agent_instructions",
3112 "human_intent",
3113 "budgeting",
3114 "self_budget",
3115 "noop",
3116 "current_time",
3117 "session_file_system",
3118 "session_storage",
3119 "session",
3120 "stateless_todo_list",
3121 "bashkit_shell",
3122 "btw",
3123 "infinity_context",
3124 "compaction",
3125 "message_metadata",
3126 "openai_tool_search",
3127 "claude_tool_search",
3128 "tool_search",
3129 "auto_tool_search",
3130 "prompt_caching",
3131 "parallel_tool_calls",
3132 "skills",
3133 "system_commands",
3134 "tool_output_persistence",
3135 "tool_output_distillation",
3136 "loop_detection",
3137 "tool_call_repair",
3138 "error_disclosure",
3139 "prompt_canary_guardrail",
3140 "guardrails",
3141 "user_hooks",
3142 ]
3143 .into_iter()
3144 .collect::<BTreeSet<_>>();
3145 if cfg!(feature = "web-fetch") {
3146 ids.insert("web_fetch");
3147 }
3148 ids
3149 }
3150
3151 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3153 let mut ids = expected_core_builtin_ids();
3154 ids.insert("agent_handoff");
3155 ids.insert("a2a_agent_delegation");
3156 ids
3157 }
3158
3159 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3160 registry.capabilities.keys().map(String::as_str).collect()
3161 }
3162
3163 #[test]
3173 fn test_capability_registry_with_builtins_dev() {
3174 let _lock = lock_env();
3176 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3177 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3178 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3179 assert!(registry.has("agent_handoff"));
3180 assert!(registry.has("a2a_agent_delegation"));
3181 }
3182
3183 #[test]
3184 fn test_capability_registry_with_builtins_prod() {
3185 let _lock = lock_env();
3187 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3188 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3189 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3190 assert!(!registry.has("docker_container"));
3192 assert!(!registry.has("agent_handoff"));
3193 assert!(!registry.has("a2a_agent_delegation"));
3194 }
3195
3196 #[test]
3197 fn test_capability_registry_runtime_builtins() {
3198 let _lock = lock_env();
3199 unsafe { std::env::remove_var("FEATURE_LUA") };
3200 let registry = CapabilityRegistry::runtime_builtins();
3201 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3202 assert!(registry.has("session_file_system"));
3203 #[cfg(feature = "web-fetch")]
3204 assert!(registry.has("web_fetch"));
3205 assert!(registry.has("bashkit_shell"));
3206
3207 for platform_only in [
3208 "platform_management",
3209 "model_scout",
3210 "openrouter_workspace",
3211 "openrouter_server_tools",
3212 "session_tasks",
3213 "session_schedule",
3214 "subagents",
3215 "background_execution",
3216 "session_sql_database",
3217 "knowledge_base",
3218 "knowledge_index",
3219 "sample_data",
3220 "data_knowledge",
3221 "fake_aws",
3222 "fake_crm",
3223 "fake_financial",
3224 "fake_warehouse",
3225 "test_math",
3226 "test_weather",
3227 "research",
3228 ] {
3229 assert!(
3230 !registry.has(platform_only),
3231 "`{platform_only}` should not be in the runtime default registry"
3232 );
3233 }
3234 }
3235
3236 #[test]
3237 fn test_agent_delegation_enabled_by_env_in_prod() {
3238 let _lock = lock_env();
3240 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3241 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3242 assert!(registry.has("agent_handoff"));
3243 assert!(registry.has("a2a_agent_delegation"));
3244 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3245 }
3246
3247 #[test]
3248 fn test_agent_delegation_disabled_by_env_in_dev() {
3249 let _lock = lock_env();
3251 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3252 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3253 assert!(!registry.has("agent_handoff"));
3254 assert!(!registry.has("a2a_agent_delegation"));
3255 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3256 }
3257
3258 #[test]
3259 fn test_capability_registry_get() {
3260 let registry = CapabilityRegistry::with_builtins();
3261
3262 let noop = registry.get("noop").unwrap();
3263 assert_eq!(noop.id(), "noop");
3264 assert_eq!(noop.name(), "No-Op");
3265 assert_eq!(noop.status(), CapabilityStatus::Available);
3266 }
3267
3268 #[test]
3276 fn builtin_capabilities_satisfy_registry_invariants() {
3277 let registry = CapabilityRegistry::with_builtins();
3278
3279 for cap in registry.list() {
3280 let id = cap.id();
3281 assert!(!id.is_empty(), "capability has an empty id");
3282 assert!(
3283 !cap.name().trim().is_empty(),
3284 "capability `{id}` has an empty name"
3285 );
3286
3287 assert!(
3290 registry.get(id).is_some(),
3291 "capability `{id}` does not resolve by its own id"
3292 );
3293
3294 for dep in cap.dependencies() {
3298 assert!(
3299 registry.get(dep).is_some(),
3300 "capability `{id}` depends on `{dep}`, which is not registered"
3301 );
3302 }
3303
3304 let mut seen = std::collections::HashSet::new();
3307 for tool in cap.tools() {
3308 let name = tool.name().to_string();
3309 assert!(
3310 !name.is_empty(),
3311 "capability `{id}` exposes a tool with an empty name"
3312 );
3313 assert!(
3314 seen.insert(name.clone()),
3315 "capability `{id}` exposes duplicate tool name `{name}`"
3316 );
3317 }
3318
3319 let mut def_seen = std::collections::HashSet::new();
3322 for def in cap.tool_definitions() {
3323 let name = def.name().to_string();
3324 assert!(
3325 !name.is_empty(),
3326 "capability `{id}` advertises a tool definition with an empty name"
3327 );
3328 assert!(
3329 def_seen.insert(name.clone()),
3330 "capability `{id}` advertises duplicate tool definition name `{name}`"
3331 );
3332 }
3333 }
3334 }
3335
3336 #[test]
3337 fn test_capability_registry_blueprint_with_capability() {
3338 struct BlueprintProviderCapability;
3339
3340 impl Capability for BlueprintProviderCapability {
3341 fn id(&self) -> &str {
3342 "blueprint_provider"
3343 }
3344 fn name(&self) -> &str {
3345 "Blueprint Provider"
3346 }
3347 fn description(&self) -> &str {
3348 "Capability that provides a blueprint for tests"
3349 }
3350 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3351 vec![AgentBlueprint {
3352 id: "test_blueprint",
3353 name: "Test Blueprint",
3354 description: "Blueprint for capability registry tests",
3355 model: BlueprintModel::Inherit,
3356 system_prompt: "Test prompt",
3357 tools: vec![],
3358 max_turns: None,
3359 config_schema: None,
3360 }]
3361 }
3362 }
3363
3364 let mut registry = CapabilityRegistry::new();
3365 registry.register(BlueprintProviderCapability);
3366
3367 let (capability_id, blueprint) = registry
3368 .blueprint_with_capability("test_blueprint")
3369 .expect("blueprint should resolve with capability id");
3370 assert_eq!(capability_id, "blueprint_provider");
3371 assert_eq!(blueprint.id, "test_blueprint");
3372 }
3373
3374 #[test]
3375 fn test_capability_registry_builder() {
3376 let registry = CapabilityRegistry::builder()
3377 .capability(NoopCapability)
3378 .capability(CurrentTimeCapability)
3379 .build();
3380
3381 assert!(registry.has("noop"));
3382 assert!(registry.has("current_time"));
3383 assert_eq!(registry.len(), 2);
3384 }
3385
3386 #[test]
3387 fn test_capability_status() {
3388 let registry = CapabilityRegistry::with_builtins();
3389
3390 let current_time = registry.get("current_time").unwrap();
3391 assert_eq!(current_time.status(), CapabilityStatus::Available);
3392
3393 let research = registry.get("research").unwrap();
3394 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3395 }
3396
3397 #[test]
3398 fn test_capability_icons_and_categories() {
3399 let registry = CapabilityRegistry::with_builtins();
3400
3401 let noop = registry.get("noop").unwrap();
3402 assert_eq!(noop.icon(), Some("circle-off"));
3403 assert_eq!(noop.category(), Some("Testing"));
3404
3405 let current_time = registry.get("current_time").unwrap();
3406 assert_eq!(current_time.icon(), Some("clock"));
3407 assert_eq!(current_time.category(), Some("Core"));
3408 }
3409
3410 #[test]
3411 fn test_system_prompt_preview_default_delegates_to_addition() {
3412 let registry = CapabilityRegistry::with_builtins();
3413
3414 let test_math = registry.get("test_math").unwrap();
3416 assert_eq!(
3417 test_math.system_prompt_preview().as_deref(),
3418 test_math.system_prompt_addition()
3419 );
3420
3421 let current_time = registry.get("current_time").unwrap();
3423 assert!(current_time.system_prompt_preview().is_none());
3424 assert!(current_time.system_prompt_addition().is_none());
3425 }
3426
3427 #[test]
3428 fn test_system_prompt_preview_dynamic_capability() {
3429 let registry = CapabilityRegistry::with_builtins();
3430 let cap = registry.get("agent_instructions").unwrap();
3431
3432 assert!(cap.system_prompt_addition().is_none());
3434 assert!(cap.system_prompt_preview().is_some());
3435 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3436 }
3437
3438 #[tokio::test]
3443 async fn test_apply_capabilities_empty() {
3444 let registry = CapabilityRegistry::with_builtins();
3445 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3446
3447 let applied =
3448 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3449
3450 assert_eq!(
3451 applied.runtime_agent.system_prompt,
3452 base_runtime_agent.system_prompt
3453 );
3454 assert!(applied.tool_registry.is_empty());
3455 assert!(applied.applied_ids.is_empty());
3456 }
3457
3458 #[tokio::test]
3459 async fn test_apply_capabilities_noop() {
3460 let registry = CapabilityRegistry::with_builtins();
3461 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3462
3463 let applied = apply_capabilities(
3464 base_runtime_agent.clone(),
3465 &["noop".to_string()],
3466 ®istry,
3467 &test_ctx(),
3468 )
3469 .await;
3470
3471 assert_eq!(
3473 applied.runtime_agent.system_prompt,
3474 base_runtime_agent.system_prompt
3475 );
3476 assert!(applied.tool_registry.is_empty());
3477 assert_eq!(applied.applied_ids, vec!["noop"]);
3478 }
3479
3480 #[tokio::test]
3481 async fn test_apply_capabilities_current_time() {
3482 let registry = CapabilityRegistry::with_builtins();
3483 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3484
3485 let applied = apply_capabilities(
3486 base_runtime_agent.clone(),
3487 &["current_time".to_string()],
3488 ®istry,
3489 &test_ctx(),
3490 )
3491 .await;
3492
3493 assert!(
3497 applied
3498 .runtime_agent
3499 .system_prompt
3500 .contains(FACTS_DYNAMIC_NOTE),
3501 "current_time should contribute the dynamic-facts note"
3502 );
3503 assert!(
3504 applied
3505 .runtime_agent
3506 .system_prompt
3507 .contains(&base_runtime_agent.system_prompt),
3508 "base prompt is preserved"
3509 );
3510 assert!(applied.tool_registry.has("get_current_time"));
3511 assert_eq!(applied.tool_registry.len(), 1);
3512 assert_eq!(applied.applied_ids, vec!["current_time"]);
3513 }
3514
3515 #[tokio::test]
3516 async fn test_apply_capabilities_skips_coming_soon() {
3517 let registry = CapabilityRegistry::with_builtins();
3518 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3519
3520 let applied = apply_capabilities(
3522 base_runtime_agent.clone(),
3523 &["research".to_string()],
3524 ®istry,
3525 &test_ctx(),
3526 )
3527 .await;
3528
3529 assert_eq!(
3531 applied.runtime_agent.system_prompt,
3532 base_runtime_agent.system_prompt
3533 );
3534 assert!(applied.applied_ids.is_empty()); }
3536
3537 #[tokio::test]
3538 async fn test_apply_capabilities_multiple() {
3539 let registry = CapabilityRegistry::with_builtins();
3540 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3541
3542 let applied = apply_capabilities(
3543 base_runtime_agent.clone(),
3544 &["noop".to_string(), "current_time".to_string()],
3545 ®istry,
3546 &test_ctx(),
3547 )
3548 .await;
3549
3550 assert!(applied.tool_registry.has("get_current_time"));
3551 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3552 }
3553
3554 #[tokio::test]
3555 async fn test_apply_capabilities_preserves_order() {
3556 let registry = CapabilityRegistry::with_builtins();
3557 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3558
3559 let applied = apply_capabilities(
3561 base_runtime_agent,
3562 &["current_time".to_string(), "noop".to_string()],
3563 ®istry,
3564 &test_ctx(),
3565 )
3566 .await;
3567
3568 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3569 }
3570
3571 #[tokio::test]
3572 async fn test_apply_capabilities_test_math() {
3573 let registry = CapabilityRegistry::with_builtins();
3574 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3575
3576 let applied = apply_capabilities(
3577 base_runtime_agent.clone(),
3578 &["test_math".to_string()],
3579 ®istry,
3580 &test_ctx(),
3581 )
3582 .await;
3583
3584 assert!(
3586 !applied
3587 .runtime_agent
3588 .system_prompt
3589 .contains("<capability id=\"test_math\">")
3590 );
3591 assert!(
3593 applied
3594 .runtime_agent
3595 .system_prompt
3596 .contains("You are a helpful assistant.")
3597 );
3598 assert!(applied.tool_registry.has("add"));
3599 assert!(applied.tool_registry.has("subtract"));
3600 assert!(applied.tool_registry.has("multiply"));
3601 assert!(applied.tool_registry.has("divide"));
3602 assert_eq!(applied.tool_registry.len(), 4);
3603 }
3604
3605 #[tokio::test]
3606 async fn test_apply_capabilities_test_weather() {
3607 let registry = CapabilityRegistry::with_builtins();
3608 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3609
3610 let applied = apply_capabilities(
3611 base_runtime_agent.clone(),
3612 &["test_weather".to_string()],
3613 ®istry,
3614 &test_ctx(),
3615 )
3616 .await;
3617
3618 assert!(
3620 !applied
3621 .runtime_agent
3622 .system_prompt
3623 .contains("<capability id=\"test_weather\">")
3624 );
3625 assert!(applied.tool_registry.has("get_weather"));
3626 assert!(applied.tool_registry.has("get_forecast"));
3627 assert_eq!(applied.tool_registry.len(), 2);
3628 }
3629
3630 #[tokio::test]
3631 async fn test_apply_capabilities_test_math_and_test_weather() {
3632 let registry = CapabilityRegistry::with_builtins();
3633 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3634
3635 let applied = apply_capabilities(
3636 base_runtime_agent.clone(),
3637 &["test_math".to_string(), "test_weather".to_string()],
3638 ®istry,
3639 &test_ctx(),
3640 )
3641 .await;
3642
3643 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3646 assert!(applied.tool_registry.has("get_weather"));
3647 }
3648
3649 #[tokio::test]
3650 async fn test_apply_capabilities_stateless_todo_list() {
3651 let registry = CapabilityRegistry::with_builtins();
3652 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3653
3654 let applied = apply_capabilities(
3655 base_runtime_agent.clone(),
3656 &["stateless_todo_list".to_string()],
3657 ®istry,
3658 &test_ctx(),
3659 )
3660 .await;
3661
3662 assert!(
3664 applied
3665 .runtime_agent
3666 .system_prompt
3667 .contains("Task Management")
3668 );
3669 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3670 assert!(applied.tool_registry.has("write_todos"));
3671 assert_eq!(applied.tool_registry.len(), 1);
3672 }
3673
3674 #[tokio::test]
3675 async fn test_apply_capabilities_web_fetch() {
3676 let registry = CapabilityRegistry::with_builtins();
3677 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3678
3679 let applied = apply_capabilities(
3680 base_runtime_agent.clone(),
3681 &["web_fetch".to_string()],
3682 ®istry,
3683 &test_ctx(),
3684 )
3685 .await;
3686
3687 assert!(
3689 applied
3690 .runtime_agent
3691 .system_prompt
3692 .contains(&base_runtime_agent.system_prompt)
3693 );
3694 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3695 assert!(applied.tool_registry.has("web_fetch"));
3696 assert_eq!(applied.tool_registry.len(), 1);
3697 }
3698
3699 #[tokio::test]
3704 async fn test_xml_tags_wrap_capability_prompts() {
3705 let registry = CapabilityRegistry::with_builtins();
3706 let collected =
3707 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3708 .await;
3709
3710 assert_eq!(collected.system_prompt_parts.len(), 1);
3711 let part = &collected.system_prompt_parts[0];
3712 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3713 assert!(part.ends_with("</capability>"));
3714 assert!(part.contains("Task Management"));
3715 }
3716
3717 #[tokio::test]
3718 async fn test_xml_tags_multiple_capabilities() {
3719 let registry = CapabilityRegistry::with_builtins();
3720 let collected = collect_capabilities(
3721 &[
3722 "stateless_todo_list".to_string(),
3723 "session_schedule".to_string(),
3724 ],
3725 ®istry,
3726 &test_ctx(),
3727 )
3728 .await;
3729
3730 assert_eq!(collected.system_prompt_parts.len(), 2);
3731 assert!(
3732 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3733 );
3734 assert!(
3735 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3736 );
3737
3738 let prefix = collected.system_prompt_prefix().unwrap();
3739 assert!(prefix.contains("</capability>\n\n<capability"));
3741 }
3742
3743 #[tokio::test]
3744 async fn test_xml_tags_system_prompt_wrapping() {
3745 let registry = CapabilityRegistry::with_builtins();
3746 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3747
3748 let applied = apply_capabilities(
3749 base,
3750 &["stateless_todo_list".to_string()],
3751 ®istry,
3752 &test_ctx(),
3753 )
3754 .await;
3755
3756 let prompt = &applied.runtime_agent.system_prompt;
3757 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3758 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3760 assert!(prompt.contains("</capability>"));
3761 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3763 }
3764
3765 #[tokio::test]
3766 async fn test_no_xml_wrapping_without_capabilities() {
3767 let registry = CapabilityRegistry::with_builtins();
3768 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3769
3770 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3771
3772 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3774 assert!(
3775 !applied
3776 .runtime_agent
3777 .system_prompt
3778 .contains("<system-prompt>")
3779 );
3780 }
3781
3782 #[tokio::test]
3783 async fn test_no_xml_wrapping_for_noop_capability() {
3784 let registry = CapabilityRegistry::with_builtins();
3785 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3786
3787 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3789
3790 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3791 assert!(
3792 !applied
3793 .runtime_agent
3794 .system_prompt
3795 .contains("<system-prompt>")
3796 );
3797 }
3798
3799 #[tokio::test]
3804 async fn test_collect_capabilities_includes_mounts() {
3805 let registry = CapabilityRegistry::with_builtins();
3806
3807 let collected =
3808 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3809
3810 assert!(!collected.mounts.is_empty());
3811 assert_eq!(collected.mounts.len(), 1);
3812 assert_eq!(collected.mounts[0].path, "/samples");
3813 assert!(collected.mounts[0].is_readonly());
3814 }
3815
3816 #[tokio::test]
3817 async fn test_collect_capabilities_empty_mounts_by_default() {
3818 let registry = CapabilityRegistry::with_builtins();
3819
3820 let collected =
3822 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3823
3824 assert!(collected.mounts.is_empty());
3825 }
3826
3827 #[tokio::test]
3828 async fn test_dynamic_facts_add_note_without_static_block() {
3829 let registry = CapabilityRegistry::with_builtins();
3833 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3834 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3835 let prompt = collected.system_prompt_parts.join("\n");
3836 assert!(
3837 prompt.contains(FACTS_DYNAMIC_NOTE),
3838 "dynamic-facts note should be in the cached prompt"
3839 );
3840 assert!(
3841 !prompt.contains("<facts>\n"),
3842 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
3843 );
3844 }
3845
3846 #[tokio::test]
3847 async fn test_static_facts_fold_into_prompt() {
3848 struct StaticFactCap;
3849 impl Capability for StaticFactCap {
3850 fn id(&self) -> &str {
3851 "test_static_fact"
3852 }
3853 fn name(&self) -> &str {
3854 "Static Fact"
3855 }
3856 fn description(&self) -> &str {
3857 "test"
3858 }
3859 fn status(&self) -> CapabilityStatus {
3860 CapabilityStatus::Available
3861 }
3862 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
3863 vec![Fact::stat("workspace_root", "/workspace")]
3864 }
3865 }
3866 let mut registry = CapabilityRegistry::new();
3867 registry.register(StaticFactCap);
3868 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
3869 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3870 let prompt = collected.system_prompt_parts.join("\n");
3871 assert!(
3872 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
3873 "static fact should fold into the cached prompt; got: {prompt}"
3874 );
3875 assert!(
3876 !prompt.contains(FACTS_DYNAMIC_NOTE),
3877 "no dynamic note when only static facts exist"
3878 );
3879 }
3880
3881 #[test]
3882 fn test_collect_dynamic_facts_returns_current_time() {
3883 let registry = CapabilityRegistry::with_builtins();
3884 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3885 let facts = collect_dynamic_facts(
3886 &configs,
3887 ®istry,
3888 None,
3889 &FactsContext::new(SessionId::new()),
3890 );
3891 assert_eq!(facts.len(), 1);
3892 assert_eq!(facts[0].key, "current_time");
3893 assert_eq!(facts[0].volatility, Volatility::Dynamic);
3894 }
3895
3896 #[tokio::test]
3897 async fn test_collect_capabilities_combines_mounts() {
3898 let registry = CapabilityRegistry::with_builtins();
3899
3900 let collected = collect_capabilities(
3903 &["sample_data".to_string(), "current_time".to_string()],
3904 ®istry,
3905 &test_ctx(),
3906 )
3907 .await;
3908
3909 assert_eq!(collected.mounts.len(), 1);
3910 assert!(
3912 collected
3913 .applied_ids
3914 .iter()
3915 .any(|id| id == "session_file_system")
3916 );
3917 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3918 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3919 }
3920
3921 #[test]
3922 fn test_sample_data_capability() {
3923 let registry = CapabilityRegistry::with_builtins();
3924 let cap = registry.get("sample_data").unwrap();
3925
3926 assert_eq!(cap.id(), "sample_data");
3927 assert_eq!(cap.name(), "Sample Data");
3928 assert_eq!(cap.status(), CapabilityStatus::Available);
3929
3930 assert!(cap.system_prompt_addition().is_some());
3932 assert!(cap.tools().is_empty());
3933
3934 assert!(!cap.mounts().is_empty());
3936 }
3937
3938 #[test]
3943 fn test_resolve_dependencies_empty() {
3944 let registry = CapabilityRegistry::with_builtins();
3945
3946 let resolved = resolve_dependencies(&[], ®istry).unwrap();
3947
3948 assert!(resolved.resolved_ids.is_empty());
3949 assert!(resolved.added_as_dependencies.is_empty());
3950 assert!(resolved.user_selected.is_empty());
3951 }
3952
3953 #[test]
3954 fn test_resolve_dependencies_no_deps() {
3955 let registry = CapabilityRegistry::with_builtins();
3956
3957 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
3959
3960 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
3961 assert!(resolved.added_as_dependencies.is_empty());
3962 }
3963
3964 #[test]
3965 fn test_resolve_dependencies_with_deps() {
3966 let registry = CapabilityRegistry::with_builtins();
3967
3968 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
3970
3971 assert_eq!(resolved.resolved_ids.len(), 2);
3973 let fs_pos = resolved
3974 .resolved_ids
3975 .iter()
3976 .position(|id| id == "session_file_system")
3977 .unwrap();
3978 let sd_pos = resolved
3979 .resolved_ids
3980 .iter()
3981 .position(|id| id == "sample_data")
3982 .unwrap();
3983 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
3984
3985 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
3987 }
3988
3989 #[test]
3990 fn test_resolve_dependencies_already_selected() {
3991 let registry = CapabilityRegistry::with_builtins();
3992
3993 let resolved = resolve_dependencies(
3995 &["session_file_system".to_string(), "sample_data".to_string()],
3996 ®istry,
3997 )
3998 .unwrap();
3999
4000 assert_eq!(resolved.resolved_ids.len(), 2);
4001 assert!(resolved.added_as_dependencies.is_empty());
4003 }
4004
4005 #[test]
4006 fn test_resolve_dependencies_preserves_order() {
4007 let registry = CapabilityRegistry::with_builtins();
4008
4009 let resolved =
4011 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4012 .unwrap();
4013
4014 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4015 }
4016
4017 #[test]
4018 fn test_resolve_dependencies_unknown_capability() {
4019 let registry = CapabilityRegistry::with_builtins();
4020
4021 let resolved =
4023 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4024
4025 assert!(resolved.resolved_ids.is_empty());
4026 }
4027
4028 #[test]
4029 fn test_get_dependencies() {
4030 let registry = CapabilityRegistry::with_builtins();
4031
4032 let deps = get_dependencies("sample_data", ®istry);
4034 assert_eq!(deps, vec!["session_file_system"]);
4035
4036 let deps = get_dependencies("current_time", ®istry);
4038 assert!(deps.is_empty());
4039
4040 let deps = get_dependencies("unknown", ®istry);
4042 assert!(deps.is_empty());
4043 }
4044
4045 #[test]
4046 fn test_sample_data_has_dependency() {
4047 let registry = CapabilityRegistry::with_builtins();
4048 let cap = registry.get("sample_data").unwrap();
4049
4050 let deps = cap.dependencies();
4051 assert_eq!(deps.len(), 1);
4052 assert_eq!(deps[0], "session_file_system");
4053 }
4054
4055 #[test]
4056 fn test_noop_has_no_dependencies() {
4057 let registry = CapabilityRegistry::with_builtins();
4058 let cap = registry.get("noop").unwrap();
4059
4060 assert!(cap.dependencies().is_empty());
4061 }
4062
4063 #[test]
4067 fn test_circular_dependency_error() {
4068 struct CapA;
4070 struct CapB;
4071
4072 impl Capability for CapA {
4073 fn id(&self) -> &str {
4074 "test_cap_a"
4075 }
4076 fn name(&self) -> &str {
4077 "Test A"
4078 }
4079 fn description(&self) -> &str {
4080 "Test capability A"
4081 }
4082 fn dependencies(&self) -> Vec<&'static str> {
4083 vec!["test_cap_b"]
4084 }
4085 }
4086
4087 impl Capability for CapB {
4088 fn id(&self) -> &str {
4089 "test_cap_b"
4090 }
4091 fn name(&self) -> &str {
4092 "Test B"
4093 }
4094 fn description(&self) -> &str {
4095 "Test capability B"
4096 }
4097 fn dependencies(&self) -> Vec<&'static str> {
4098 vec!["test_cap_a"]
4099 }
4100 }
4101
4102 let mut registry = CapabilityRegistry::new();
4103 registry.register(CapA);
4104 registry.register(CapB);
4105
4106 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4107
4108 assert!(result.is_err());
4109 match result.unwrap_err() {
4110 DependencyError::CircularDependency { capability_id, .. } => {
4111 assert_eq!(capability_id, "test_cap_a");
4112 }
4113 _ => panic!("Expected CircularDependency error"),
4114 }
4115 }
4116
4117 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4122
4123 struct FilterTestCapability {
4125 priority: i32,
4126 }
4127
4128 impl Capability for FilterTestCapability {
4129 fn id(&self) -> &str {
4130 "filter_test"
4131 }
4132 fn name(&self) -> &str {
4133 "Filter Test"
4134 }
4135 fn description(&self) -> &str {
4136 "Test capability with message filter"
4137 }
4138 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4139 Some(Arc::new(FilterTestProvider {
4140 priority: self.priority,
4141 }))
4142 }
4143 }
4144
4145 struct FilterTestProvider {
4146 priority: i32,
4147 }
4148
4149 impl MessageFilterProvider for FilterTestProvider {
4150 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4151 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4153 query
4154 .filters
4155 .push(MessageFilter::Search(search.to_string()));
4156 }
4157 }
4158
4159 fn priority(&self) -> i32 {
4160 self.priority
4161 }
4162 }
4163
4164 #[tokio::test]
4165 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4166 let registry = CapabilityRegistry::with_builtins();
4167 let configs = vec![AgentCapabilityConfig {
4168 capability_ref: CapabilityId::new("current_time"),
4169 config: serde_json::json!({}),
4170 }];
4171
4172 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4173
4174 assert!(collected.message_filter_providers.is_empty());
4175 assert!(!collected.has_message_filters());
4176 }
4177
4178 #[tokio::test]
4179 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4180 let mut registry = CapabilityRegistry::new();
4181 registry.register(FilterTestCapability { priority: 0 });
4182
4183 let configs = vec![AgentCapabilityConfig {
4184 capability_ref: CapabilityId::new("filter_test"),
4185 config: serde_json::json!({ "search": "hello" }),
4186 }];
4187
4188 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4189
4190 assert_eq!(collected.message_filter_providers.len(), 1);
4191 assert!(collected.has_message_filters());
4192 }
4193
4194 #[tokio::test]
4195 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4196 struct HighPriorityCapability;
4198 struct LowPriorityCapability;
4199
4200 impl Capability for HighPriorityCapability {
4201 fn id(&self) -> &str {
4202 "high_priority"
4203 }
4204 fn name(&self) -> &str {
4205 "High Priority"
4206 }
4207 fn description(&self) -> &str {
4208 "Test"
4209 }
4210 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4211 Some(Arc::new(FilterTestProvider { priority: 10 }))
4212 }
4213 }
4214
4215 impl Capability for LowPriorityCapability {
4216 fn id(&self) -> &str {
4217 "low_priority"
4218 }
4219 fn name(&self) -> &str {
4220 "Low Priority"
4221 }
4222 fn description(&self) -> &str {
4223 "Test"
4224 }
4225 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4226 Some(Arc::new(FilterTestProvider { priority: -5 }))
4227 }
4228 }
4229
4230 let mut registry = CapabilityRegistry::new();
4231 registry.register(HighPriorityCapability);
4232 registry.register(LowPriorityCapability);
4233
4234 let configs = vec![
4236 AgentCapabilityConfig {
4237 capability_ref: CapabilityId::new("high_priority"),
4238 config: serde_json::json!({}),
4239 },
4240 AgentCapabilityConfig {
4241 capability_ref: CapabilityId::new("low_priority"),
4242 config: serde_json::json!({}),
4243 },
4244 ];
4245
4246 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4247
4248 assert_eq!(collected.message_filter_providers.len(), 2);
4250 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4251 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4252 }
4253
4254 #[tokio::test]
4255 async fn test_collected_capabilities_apply_message_filters() {
4256 let mut registry = CapabilityRegistry::new();
4257 registry.register(FilterTestCapability { priority: 0 });
4258
4259 let configs = vec![AgentCapabilityConfig {
4260 capability_ref: CapabilityId::new("filter_test"),
4261 config: serde_json::json!({ "search": "test_query" }),
4262 }];
4263
4264 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4265
4266 let session_id: SessionId = Uuid::now_v7().into();
4268 let mut query = MessageQuery::new(session_id);
4269
4270 collected.apply_message_filters(&mut query);
4271
4272 assert_eq!(query.filters.len(), 1);
4274 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4275 }
4276
4277 #[tokio::test]
4278 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4279 struct SearchCapability {
4280 id: &'static str,
4281 search_term: &'static str,
4282 priority: i32,
4283 }
4284
4285 struct SearchProvider {
4286 search_term: &'static str,
4287 priority: i32,
4288 }
4289
4290 impl MessageFilterProvider for SearchProvider {
4291 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4292 query
4293 .filters
4294 .push(MessageFilter::Search(self.search_term.to_string()));
4295 }
4296
4297 fn priority(&self) -> i32 {
4298 self.priority
4299 }
4300 }
4301
4302 impl Capability for SearchCapability {
4303 fn id(&self) -> &str {
4304 self.id
4305 }
4306 fn name(&self) -> &str {
4307 "Search"
4308 }
4309 fn description(&self) -> &str {
4310 "Test"
4311 }
4312 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4313 Some(Arc::new(SearchProvider {
4314 search_term: self.search_term,
4315 priority: self.priority,
4316 }))
4317 }
4318 }
4319
4320 let mut registry = CapabilityRegistry::new();
4321 registry.register(SearchCapability {
4322 id: "cap_a",
4323 search_term: "alpha",
4324 priority: 5,
4325 });
4326 registry.register(SearchCapability {
4327 id: "cap_b",
4328 search_term: "beta",
4329 priority: 1,
4330 });
4331 registry.register(SearchCapability {
4332 id: "cap_c",
4333 search_term: "gamma",
4334 priority: 10,
4335 });
4336
4337 let configs = vec![
4338 AgentCapabilityConfig {
4339 capability_ref: CapabilityId::new("cap_a"),
4340 config: serde_json::json!({}),
4341 },
4342 AgentCapabilityConfig {
4343 capability_ref: CapabilityId::new("cap_b"),
4344 config: serde_json::json!({}),
4345 },
4346 AgentCapabilityConfig {
4347 capability_ref: CapabilityId::new("cap_c"),
4348 config: serde_json::json!({}),
4349 },
4350 ];
4351
4352 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4353
4354 let session_id: SessionId = Uuid::now_v7().into();
4355 let mut query = MessageQuery::new(session_id);
4356
4357 collected.apply_message_filters(&mut query);
4358
4359 assert_eq!(query.filters.len(), 3);
4361 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4362 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4363 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4364 }
4365
4366 #[test]
4367 fn test_capability_without_message_filter_returns_none() {
4368 let registry = CapabilityRegistry::with_builtins();
4369
4370 let noop = registry.get("noop").unwrap();
4371 assert!(noop.message_filter_provider().is_none());
4372
4373 let current_time = registry.get("current_time").unwrap();
4374 assert!(current_time.message_filter_provider().is_none());
4375 }
4376
4377 #[tokio::test]
4378 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4379 let mut registry = CapabilityRegistry::new();
4380 registry.register(FilterTestCapability { priority: 0 });
4381
4382 let test_config = serde_json::json!({
4383 "search": "custom_search",
4384 "extra_field": 42
4385 });
4386
4387 let configs = vec![AgentCapabilityConfig {
4388 capability_ref: CapabilityId::new("filter_test"),
4389 config: test_config.clone(),
4390 }];
4391
4392 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4393
4394 assert_eq!(collected.message_filter_providers.len(), 1);
4396 let (_, stored_config) = &collected.message_filter_providers[0];
4397 assert_eq!(*stored_config, test_config);
4398 }
4399
4400 #[test]
4405 fn test_collect_message_filters_only_collects_filters() {
4406 let mut registry = CapabilityRegistry::new();
4407 registry.register(FilterTestCapability { priority: 0 });
4408
4409 let configs = vec![AgentCapabilityConfig {
4410 capability_ref: CapabilityId::new("filter_test"),
4411 config: serde_json::json!({ "search": "test_query" }),
4412 }];
4413
4414 let collected = collect_message_filters_only(&configs, ®istry);
4415
4416 let session_id: SessionId = Uuid::now_v7().into();
4417 let mut query = MessageQuery::new(session_id);
4418 collected.apply_message_filters(&mut query);
4419
4420 assert_eq!(query.filters.len(), 1);
4421 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4422 }
4423
4424 #[test]
4425 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4426 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4427
4428 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4430 assert_eq!(with["compaction_active"], serde_json::json!(true));
4431 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4432
4433 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4434 assert!(without.get("compaction_active").is_none());
4435
4436 let other = message_filter_config_for("other", &base, true);
4438 assert!(other.get("compaction_active").is_none());
4439
4440 let null_base = message_filter_config_for(
4442 INFINITY_CONTEXT_CAPABILITY_ID,
4443 &serde_json::Value::Null,
4444 true,
4445 );
4446 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4447 }
4448
4449 #[test]
4450 fn test_infinity_context_defers_to_compaction_end_to_end() {
4451 use crate::message::Message;
4452
4453 let mut registry = CapabilityRegistry::new();
4454 registry.register(InfinityContextCapability);
4455 registry.register(CompactionCapability);
4456
4457 let tight = serde_json::json!({
4458 "context_budget_tokens": 1,
4459 "min_recent_messages": 1
4460 });
4461
4462 let solo = vec![AgentCapabilityConfig {
4464 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4465 config: tight.clone(),
4466 }];
4467 let mut messages = vec![
4468 Message::user("task"),
4469 Message::assistant("old ".repeat(400)),
4470 Message::user("recent"),
4471 ];
4472 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4473 assert!(
4474 messages
4475 .iter()
4476 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4477 "infinity context alone should trim and notice"
4478 );
4479
4480 let both = vec![
4482 AgentCapabilityConfig {
4483 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4484 config: tight,
4485 },
4486 AgentCapabilityConfig {
4487 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4488 config: serde_json::json!({}),
4489 },
4490 ];
4491 let mut messages = vec![
4492 Message::user("task"),
4493 Message::assistant("old ".repeat(400)),
4494 Message::user("recent"),
4495 ];
4496 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4497 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4498 assert!(
4499 messages
4500 .iter()
4501 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4502 "no hidden-history notice when compaction is the active reducer"
4503 );
4504 }
4505
4506 #[test]
4507 fn test_compaction_is_enabled_detects_compaction() {
4508 let mut registry = CapabilityRegistry::new();
4509 registry.register(CompactionCapability);
4510
4511 let with_compaction = vec![AgentCapabilityConfig {
4512 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4513 config: serde_json::json!({}),
4514 }];
4515 assert!(compaction_is_enabled(&with_compaction, ®istry));
4516
4517 let without = vec![AgentCapabilityConfig {
4518 capability_ref: CapabilityId::new("current_time"),
4519 config: serde_json::json!({}),
4520 }];
4521 assert!(!compaction_is_enabled(&without, ®istry));
4522 }
4523
4524 #[test]
4525 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4526 let registry = CapabilityRegistry::new();
4527
4528 let configs = vec![AgentCapabilityConfig {
4529 capability_ref: CapabilityId::new("nonexistent"),
4530 config: serde_json::json!({}),
4531 }];
4532
4533 let collected = collect_message_filters_only(&configs, ®istry);
4534 assert!(collected.message_filter_providers.is_empty());
4535 }
4536
4537 #[test]
4538 fn test_collect_message_filters_only_preserves_priority_order() {
4539 struct PriorityFilterCap {
4540 id: &'static str,
4541 search_term: &'static str,
4542 priority: i32,
4543 }
4544
4545 struct PriorityFilterProvider {
4546 search_term: &'static str,
4547 priority: i32,
4548 }
4549
4550 impl Capability for PriorityFilterCap {
4551 fn id(&self) -> &str {
4552 self.id
4553 }
4554 fn name(&self) -> &str {
4555 self.id
4556 }
4557 fn description(&self) -> &str {
4558 "priority test"
4559 }
4560 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4561 Some(Arc::new(PriorityFilterProvider {
4562 search_term: self.search_term,
4563 priority: self.priority,
4564 }))
4565 }
4566 }
4567
4568 impl MessageFilterProvider for PriorityFilterProvider {
4569 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4570 query
4571 .filters
4572 .push(MessageFilter::Search(self.search_term.to_string()));
4573 }
4574 fn priority(&self) -> i32 {
4575 self.priority
4576 }
4577 }
4578
4579 let mut registry = CapabilityRegistry::new();
4580 registry.register(PriorityFilterCap {
4581 id: "gamma",
4582 search_term: "gamma",
4583 priority: 10,
4584 });
4585 registry.register(PriorityFilterCap {
4586 id: "alpha",
4587 search_term: "alpha",
4588 priority: 5,
4589 });
4590 registry.register(PriorityFilterCap {
4591 id: "beta",
4592 search_term: "beta",
4593 priority: 1,
4594 });
4595
4596 let configs = vec![
4597 AgentCapabilityConfig {
4598 capability_ref: CapabilityId::new("gamma"),
4599 config: serde_json::json!({}),
4600 },
4601 AgentCapabilityConfig {
4602 capability_ref: CapabilityId::new("alpha"),
4603 config: serde_json::json!({}),
4604 },
4605 AgentCapabilityConfig {
4606 capability_ref: CapabilityId::new("beta"),
4607 config: serde_json::json!({}),
4608 },
4609 ];
4610
4611 let collected = collect_message_filters_only(&configs, ®istry);
4612
4613 let session_id: SessionId = Uuid::now_v7().into();
4614 let mut query = MessageQuery::new(session_id);
4615 collected.apply_message_filters(&mut query);
4616
4617 assert_eq!(query.filters.len(), 3);
4619 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4620 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4621 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4622 }
4623
4624 #[test]
4625 fn test_collect_message_filters_only_post_load_invoked() {
4626 use crate::message::Message;
4627
4628 struct PostLoadCap;
4629 struct PostLoadProvider;
4630
4631 impl Capability for PostLoadCap {
4632 fn id(&self) -> &str {
4633 "post_load_test"
4634 }
4635 fn name(&self) -> &str {
4636 "PostLoad Test"
4637 }
4638 fn description(&self) -> &str {
4639 "test"
4640 }
4641 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4642 Some(Arc::new(PostLoadProvider))
4643 }
4644 }
4645
4646 impl MessageFilterProvider for PostLoadProvider {
4647 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4648 fn priority(&self) -> i32 {
4649 0
4650 }
4651 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4652 messages.reverse();
4654 }
4655 }
4656
4657 let mut registry = CapabilityRegistry::new();
4658 registry.register(PostLoadCap);
4659
4660 let configs = vec![AgentCapabilityConfig {
4661 capability_ref: CapabilityId::new("post_load_test"),
4662 config: serde_json::json!({}),
4663 }];
4664
4665 let collected = collect_message_filters_only(&configs, ®istry);
4666
4667 let mut messages = vec![Message::user("first"), Message::user("second")];
4668 collected.apply_post_load_filters(&mut messages);
4669
4670 assert_eq!(messages[0].text(), Some("second"));
4672 assert_eq!(messages[1].text(), Some("first"));
4673 }
4674
4675 #[test]
4676 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4677 use crate::tool_types::ToolCall;
4678
4679 fn tool_heavy_messages() -> Vec<Message> {
4680 let mut messages = vec![Message::user("inspect files repeatedly")];
4681 for index in 0..9 {
4682 let call_id = format!("call_{index}");
4683 messages.push(Message::assistant_with_tools(
4684 "",
4685 vec![ToolCall {
4686 id: call_id.clone(),
4687 name: "read_file".to_string(),
4688 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4689 }],
4690 ));
4691 messages.push(Message::tool_result(
4692 call_id,
4693 Some(serde_json::json!({
4694 "path": "/workspace/src/lib.rs",
4695 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4696 "total_lines": 1000,
4697 "lines_shown": {"start": 1, "end": 1000},
4698 "truncated": false
4699 })),
4700 None,
4701 ));
4702 }
4703 messages
4704 }
4705
4706 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4707 messages[2]
4708 .tool_result_content()
4709 .and_then(|result| result.result.as_ref())
4710 .and_then(|result| result.get("masked"))
4711 .and_then(|masked| masked.as_bool())
4712 .unwrap_or(false)
4713 }
4714
4715 let mut registry = CapabilityRegistry::new();
4716 registry.register(CompactionCapability);
4717 let context = ModelViewContext {
4718 session_id: SessionId::new(),
4719 prior_usage: None,
4720 };
4721
4722 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4723 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4724 assert!(!first_tool_result_is_masked(&unmasked));
4725
4726 let compaction = collect_model_view_providers(
4727 &[AgentCapabilityConfig {
4728 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4729 config: serde_json::json!({}),
4730 }],
4731 ®istry,
4732 None,
4733 );
4734 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4735 assert!(first_tool_result_is_masked(&masked));
4736 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4737 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4738 }
4739
4740 struct DelegatingFilterCap {
4743 id: &'static str,
4744 inner: std::sync::Arc<InnerFilterCap>,
4745 }
4746 struct InnerFilterCap;
4747
4748 impl Capability for InnerFilterCap {
4749 fn id(&self) -> &str {
4750 "inner_filter"
4751 }
4752 fn name(&self) -> &str {
4753 "Inner Filter"
4754 }
4755 fn description(&self) -> &str {
4756 "inner"
4757 }
4758 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4759 Some(std::sync::Arc::new(SentinelFilter))
4760 }
4761 }
4762 struct SentinelFilter;
4763 impl MessageFilterProvider for SentinelFilter {
4764 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4765 }
4766 impl Capability for DelegatingFilterCap {
4767 fn id(&self) -> &str {
4768 self.id
4769 }
4770 fn name(&self) -> &str {
4771 "Delegating Filter"
4772 }
4773 fn description(&self) -> &str {
4774 "delegating"
4775 }
4776 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4777 None }
4779 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4780 Some(&*self.inner)
4781 }
4782 }
4783
4784 #[test]
4785 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4786 let inner = std::sync::Arc::new(InnerFilterCap);
4787 let outer = DelegatingFilterCap {
4788 id: "delegating_filter",
4789 inner: inner.clone(),
4790 };
4791
4792 let mut registry = CapabilityRegistry::new();
4793 registry.register(outer);
4794
4795 let configs = vec![AgentCapabilityConfig {
4796 capability_ref: CapabilityId::new("delegating_filter"),
4797 config: serde_json::json!({}),
4798 }];
4799
4800 let collected = collect_message_filters_only(&configs, ®istry);
4803 assert_eq!(
4804 collected.message_filter_providers.len(),
4805 1,
4806 "provider from resolved inner capability must be collected"
4807 );
4808 }
4809
4810 struct DelegatingMvpCap {
4811 id: &'static str,
4812 inner: std::sync::Arc<InnerMvpCap>,
4813 }
4814 struct InnerMvpCap;
4815
4816 impl Capability for InnerMvpCap {
4817 fn id(&self) -> &str {
4818 "inner_mvp"
4819 }
4820 fn name(&self) -> &str {
4821 "Inner MVP"
4822 }
4823 fn description(&self) -> &str {
4824 "inner"
4825 }
4826 fn model_view_provider(
4827 &self,
4828 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4829 struct NoopMvp;
4831 impl crate::capabilities::ModelViewProvider for NoopMvp {
4832 fn apply_model_view(
4833 &self,
4834 messages: Vec<Message>,
4835 _config: &serde_json::Value,
4836 _context: &ModelViewContext<'_>,
4837 ) -> Vec<Message> {
4838 messages
4839 }
4840 }
4841 Some(std::sync::Arc::new(NoopMvp))
4842 }
4843 }
4844 impl Capability for DelegatingMvpCap {
4845 fn id(&self) -> &str {
4846 self.id
4847 }
4848 fn name(&self) -> &str {
4849 "Delegating MVP"
4850 }
4851 fn description(&self) -> &str {
4852 "delegating"
4853 }
4854 fn model_view_provider(
4855 &self,
4856 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4857 None }
4859 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4860 Some(&*self.inner)
4861 }
4862 }
4863
4864 #[test]
4865 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4866 let inner = std::sync::Arc::new(InnerMvpCap);
4867 let outer = DelegatingMvpCap {
4868 id: "delegating_mvp",
4869 inner: inner.clone(),
4870 };
4871
4872 let mut registry = CapabilityRegistry::new();
4873 registry.register(outer);
4874
4875 let configs = vec![AgentCapabilityConfig {
4876 capability_ref: CapabilityId::new("delegating_mvp"),
4877 config: serde_json::json!({}),
4878 }];
4879
4880 let collected = collect_model_view_providers(&configs, ®istry, None);
4883 assert_eq!(
4884 collected.model_view_providers.len(),
4885 1,
4886 "provider from resolved inner capability must be collected"
4887 );
4888 }
4889
4890 #[tokio::test]
4900 async fn test_bashkit_shell_capability_produces_bash_tool() {
4901 let registry = CapabilityRegistry::with_builtins();
4902 let collected =
4903 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4904
4905 let tool_names: Vec<&str> = collected
4906 .tool_definitions
4907 .iter()
4908 .map(|t| t.name())
4909 .collect();
4910 assert!(
4911 tool_names.contains(&"bash"),
4912 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4913 tool_names
4914 );
4915 assert!(
4916 !collected.tools.is_empty(),
4917 "bashkit_shell must provide tool implementations"
4918 );
4919 }
4920
4921 #[tokio::test]
4922 async fn test_generic_harness_capability_set_produces_bash_tool() {
4923 let generic_harness_caps = vec![
4926 "session_file_system".to_string(),
4927 "bashkit_shell".to_string(),
4928 "web_fetch".to_string(),
4929 "session_storage".to_string(),
4930 "session".to_string(),
4931 "agent_instructions".to_string(),
4932 "skills".to_string(),
4933 "infinity_context".to_string(),
4934 "auto_tool_search".to_string(),
4935 ];
4936
4937 let registry = CapabilityRegistry::with_builtins();
4938 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
4939
4940 let tool_names: Vec<&str> = collected
4941 .tool_definitions
4942 .iter()
4943 .map(|t| t.name())
4944 .collect();
4945 assert!(
4946 tool_names.contains(&"bash"),
4947 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
4948 tool_names
4949 );
4950 }
4951
4952 #[tokio::test]
4953 async fn test_collect_capabilities_tool_count_matches_definitions() {
4954 let registry = CapabilityRegistry::with_builtins();
4957 let collected =
4958 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4959
4960 assert_eq!(
4961 collected.tools.len(),
4962 collected.tool_definitions.len(),
4963 "tool implementations ({}) must match tool definitions ({})",
4964 collected.tools.len(),
4965 collected.tool_definitions.len(),
4966 );
4967 }
4968
4969 #[tokio::test]
4973 async fn test_collect_capabilities_resolves_dependencies() {
4974 let registry = CapabilityRegistry::with_builtins();
4977 let collected =
4978 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4979
4980 assert!(
4982 collected
4983 .applied_ids
4984 .iter()
4985 .any(|id| id == "session_file_system"),
4986 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
4987 collected.applied_ids
4988 );
4989
4990 let tool_names: Vec<&str> = collected
4991 .tool_definitions
4992 .iter()
4993 .map(|t| t.name())
4994 .collect();
4995
4996 assert!(
4998 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
4999 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5000 tool_names
5001 );
5002
5003 assert_eq!(
5005 collected.tools.len(),
5006 collected.tool_definitions.len(),
5007 "dependency-added tools must have implementations, not just definitions"
5008 );
5009 }
5010
5011 #[test]
5012 fn test_defaults_do_not_include_bash() {
5013 let registry = crate::ToolRegistry::with_defaults();
5016 assert!(
5017 !registry.has("bash"),
5018 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5019 );
5020 }
5021
5022 #[tokio::test]
5029 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5030 let registry = CapabilityRegistry::with_builtins();
5031 let collected =
5032 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5033
5034 let tool_names: Vec<&str> = collected
5035 .tool_definitions
5036 .iter()
5037 .map(|t| t.name())
5038 .collect();
5039 assert!(
5040 tool_names.contains(&"spawn_background"),
5041 "spawn_background must be auto-activated when bashkit_shell (a \
5042 background-capable tool) is in the agent's capability set; got: {:?}",
5043 tool_names
5044 );
5045 assert!(
5046 collected
5047 .applied_ids
5048 .iter()
5049 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5050 "background_execution must be in applied_ids when auto-activated; \
5051 got: {:?}",
5052 collected.applied_ids
5053 );
5054
5055 assert!(
5057 collected
5058 .tools
5059 .iter()
5060 .any(|t| t.name() == "spawn_background"),
5061 "spawn_background tool implementation must be present alongside the \
5062 definition (lockstep contract)"
5063 );
5064 }
5065
5066 #[tokio::test]
5069 async fn test_background_execution_does_not_auto_activate_without_hint() {
5070 let registry = CapabilityRegistry::with_builtins();
5071 let collected =
5073 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5074
5075 let tool_names: Vec<&str> = collected
5076 .tool_definitions
5077 .iter()
5078 .map(|t| t.name())
5079 .collect();
5080 assert!(
5081 !tool_names.contains(&"spawn_background"),
5082 "spawn_background must NOT be activated without a background-capable \
5083 tool; got: {:?}",
5084 tool_names
5085 );
5086 assert!(
5087 !collected
5088 .applied_ids
5089 .iter()
5090 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5091 "background_execution must not appear in applied_ids when no \
5092 background-capable tool is present; got: {:?}",
5093 collected.applied_ids
5094 );
5095 }
5096
5097 #[tokio::test]
5098 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5099 let registry = CapabilityRegistry::with_builtins();
5100 let collected = collect_capabilities(
5101 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5102 ®istry,
5103 &test_ctx(),
5104 )
5105 .await;
5106
5107 assert!(
5108 collected
5109 .tools
5110 .iter()
5111 .any(|tool| tool.name() == "spawn_agent"),
5112 "subagent-only sessions should get the unified spawn_agent adapter"
5113 );
5114 let spawn_agent = collected
5115 .tool_definitions
5116 .iter()
5117 .find(|tool| tool.name() == "spawn_agent")
5118 .expect("spawn_agent definition");
5119 assert_eq!(
5120 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5121 serde_json::json!(["subagent"])
5122 );
5123 }
5124
5125 #[tokio::test]
5126 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5127 let mut registry = CapabilityRegistry::new();
5128 registry.register(AgentHandoffCapability);
5129 let agent_id = crate::typed_id::AgentId::new();
5130 let harness_id = crate::typed_id::HarnessId::new();
5131 let configs = vec![AgentCapabilityConfig {
5132 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5133 config: serde_json::json!({
5134 "targets": [{
5135 "id": "aws_operator",
5136 "name": "AWS Operator",
5137 "agent_id": agent_id,
5138 "harness_id": harness_id
5139 }]
5140 }),
5141 }];
5142 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5143
5144 assert!(
5145 collected
5146 .tools
5147 .iter()
5148 .any(|tool| tool.name() == "spawn_agent"),
5149 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5150 );
5151 let spawn_agent = collected
5152 .tool_definitions
5153 .iter()
5154 .find(|tool| tool.name() == "spawn_agent")
5155 .expect("spawn_agent definition");
5156 assert_eq!(
5157 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5158 serde_json::json!(["agent"])
5159 );
5160 }
5161
5162 #[tokio::test]
5163 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5164 let mut registry = CapabilityRegistry::new();
5165 registry.register(SubagentCapability);
5166 registry.register(AgentHandoffCapability);
5167
5168 let agent_id = crate::typed_id::AgentId::new();
5169 let harness_id = crate::typed_id::HarnessId::new();
5170 let configs = vec![
5171 AgentCapabilityConfig {
5172 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5173 config: serde_json::json!({}),
5174 },
5175 AgentCapabilityConfig {
5176 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5177 config: serde_json::json!({
5178 "targets": [{
5179 "id": "aws_operator",
5180 "name": "AWS Operator",
5181 "agent_id": agent_id,
5182 "harness_id": harness_id
5183 }]
5184 }),
5185 },
5186 ];
5187
5188 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5189 let spawn_agent_defs: Vec<_> = collected
5190 .tool_definitions
5191 .iter()
5192 .filter(|tool| tool.name() == "spawn_agent")
5193 .collect();
5194
5195 assert_eq!(spawn_agent_defs.len(), 1);
5196 assert_eq!(
5197 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5198 serde_json::json!(["subagent", "agent"])
5199 );
5200 }
5201
5202 #[cfg(feature = "a2a")]
5203 #[tokio::test]
5204 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5205 let mut registry = CapabilityRegistry::new();
5206 registry.register(SubagentCapability);
5207 registry.register(A2aAgentDelegationCapability);
5208
5209 let configs = vec![
5210 AgentCapabilityConfig {
5211 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5212 config: serde_json::json!({}),
5213 },
5214 AgentCapabilityConfig {
5215 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5216 config: serde_json::json!({
5217 "agents": [{
5218 "id": "local_app",
5219 "name": "Local App",
5220 "base_url": "https://example.com"
5221 }]
5222 }),
5223 },
5224 ];
5225
5226 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5227 let spawn_agent_defs: Vec<_> = collected
5228 .tool_definitions
5229 .iter()
5230 .filter(|tool| tool.name() == "spawn_agent")
5231 .collect();
5232
5233 assert_eq!(spawn_agent_defs.len(), 1);
5234 assert_eq!(
5235 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5236 serde_json::json!(["subagent", "external_a2a"])
5237 );
5238 }
5239
5240 struct ExistingSpawnAgentCapability;
5241
5242 impl Capability for ExistingSpawnAgentCapability {
5243 fn id(&self) -> &str {
5244 "existing_spawn_agent"
5245 }
5246
5247 fn name(&self) -> &str {
5248 "Existing Spawn Agent"
5249 }
5250
5251 fn description(&self) -> &str {
5252 "Test capability that already owns spawn_agent"
5253 }
5254
5255 fn tools(&self) -> Vec<Box<dyn Tool>> {
5256 vec![Box::new(ExistingSpawnAgentTool)]
5257 }
5258 }
5259
5260 struct ExistingSpawnAgentTool;
5261
5262 #[async_trait]
5263 impl Tool for ExistingSpawnAgentTool {
5264 fn name(&self) -> &str {
5265 "spawn_agent"
5266 }
5267
5268 fn description(&self) -> &str {
5269 "Existing spawn_agent test tool"
5270 }
5271
5272 fn parameters_schema(&self) -> serde_json::Value {
5273 serde_json::json!({
5274 "type": "object",
5275 "properties": {
5276 "target": {
5277 "type": "object",
5278 "properties": {
5279 "type": {"type": "string", "enum": ["external_a2a"]}
5280 },
5281 "required": ["type"]
5282 }
5283 },
5284 "required": ["target"]
5285 })
5286 }
5287
5288 async fn execute(
5289 &self,
5290 _arguments: serde_json::Value,
5291 ) -> crate::tools::ToolExecutionResult {
5292 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5293 }
5294 }
5295
5296 #[tokio::test]
5297 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5298 let mut registry = CapabilityRegistry::new();
5299 registry.register(SubagentCapability);
5300 registry.register(ExistingSpawnAgentCapability);
5301
5302 let collected = collect_capabilities(
5303 &[
5304 SUBAGENTS_CAPABILITY_ID.to_string(),
5305 "existing_spawn_agent".to_string(),
5306 ],
5307 ®istry,
5308 &test_ctx(),
5309 )
5310 .await;
5311
5312 let spawn_agent_defs: Vec<_> = collected
5313 .tool_definitions
5314 .iter()
5315 .filter(|tool| tool.name() == "spawn_agent")
5316 .collect();
5317 assert_eq!(spawn_agent_defs.len(), 1);
5318 assert_eq!(
5319 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5320 serde_json::json!(["external_a2a"])
5321 );
5322 }
5323
5324 #[tokio::test]
5325 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5326 let mut registry = CapabilityRegistry::new();
5327 registry.register(AgentHandoffCapability);
5328 registry.register(ExistingSpawnAgentCapability);
5329
5330 let agent_id = crate::typed_id::AgentId::new();
5331 let harness_id = crate::typed_id::HarnessId::new();
5332 let configs = vec![
5333 AgentCapabilityConfig {
5334 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5335 config: serde_json::json!({
5336 "targets": [{
5337 "id": "aws_operator",
5338 "name": "AWS Operator",
5339 "agent_id": agent_id,
5340 "harness_id": harness_id
5341 }]
5342 }),
5343 },
5344 AgentCapabilityConfig {
5345 capability_ref: CapabilityId::new("existing_spawn_agent"),
5346 config: serde_json::json!({}),
5347 },
5348 ];
5349
5350 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5351
5352 let spawn_agent_defs: Vec<_> = collected
5353 .tool_definitions
5354 .iter()
5355 .filter(|tool| tool.name() == "spawn_agent")
5356 .collect();
5357 assert_eq!(spawn_agent_defs.len(), 1);
5358 assert_eq!(
5359 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5360 serde_json::json!(["external_a2a"])
5361 );
5362 }
5363
5364 #[tokio::test]
5368 async fn test_background_execution_explicit_selection_is_idempotent() {
5369 let registry = CapabilityRegistry::with_builtins();
5370 let collected = collect_capabilities(
5371 &[
5372 "bashkit_shell".to_string(),
5373 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5374 ],
5375 ®istry,
5376 &test_ctx(),
5377 )
5378 .await;
5379
5380 let spawn_background_count = collected
5381 .tool_definitions
5382 .iter()
5383 .filter(|t| t.name() == "spawn_background")
5384 .count();
5385 assert_eq!(
5386 spawn_background_count, 1,
5387 "spawn_background must appear exactly once even when \
5388 background_execution is selected explicitly alongside a \
5389 background-capable tool"
5390 );
5391 let applied_count = collected
5392 .applied_ids
5393 .iter()
5394 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5395 .count();
5396 assert_eq!(
5397 applied_count, 1,
5398 "background_execution must appear exactly once in applied_ids"
5399 );
5400 }
5401
5402 #[test]
5407 fn test_defaults_do_not_include_spawn_background() {
5408 let registry = crate::ToolRegistry::with_defaults();
5409 assert!(
5410 !registry.has("spawn_background"),
5411 "with_defaults() must not include 'spawn_background' — it comes \
5412 from the background_execution capability (EVE-501)"
5413 );
5414 }
5415
5416 #[test]
5421 fn test_capability_features_default_empty() {
5422 let registry = CapabilityRegistry::with_builtins();
5423
5424 let noop = registry.get("noop").unwrap();
5426 assert!(noop.features().is_empty());
5427
5428 let current_time = registry.get("current_time").unwrap();
5429 assert!(current_time.features().is_empty());
5430 }
5431
5432 #[test]
5433 fn test_file_system_capability_features() {
5434 let registry = CapabilityRegistry::with_builtins();
5435
5436 let fs = registry.get("session_file_system").unwrap();
5437 assert_eq!(fs.features(), vec!["file_system"]);
5438 }
5439
5440 #[test]
5441 fn test_bashkit_shell_capability_features() {
5442 let registry = CapabilityRegistry::with_builtins();
5443
5444 let bash = registry.get("bashkit_shell").unwrap();
5445 assert_eq!(bash.features(), vec!["file_system"]);
5446 }
5447
5448 #[test]
5449 fn test_alias_resolves_to_canonical_capability() {
5450 let registry = CapabilityRegistry::with_builtins();
5451
5452 let via_alias = registry.get("virtual_bash").unwrap();
5454 assert_eq!(via_alias.id(), "bashkit_shell");
5455 assert!(registry.has("virtual_bash"));
5456 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5457 assert_eq!(
5458 registry.canonical_id("bashkit_shell"),
5459 Some("bashkit_shell")
5460 );
5461 assert_eq!(registry.canonical_id("nonexistent"), None);
5462 }
5463
5464 #[test]
5465 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5466 let registry = CapabilityRegistry::with_builtins();
5467
5468 let resolved = resolve_dependencies(
5471 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5472 ®istry,
5473 )
5474 .unwrap();
5475 let bash_ids: Vec<_> = resolved
5476 .resolved_ids
5477 .iter()
5478 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5479 .collect();
5480 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5481 assert!(
5483 !resolved
5484 .added_as_dependencies
5485 .contains(&"bashkit_shell".to_string())
5486 );
5487 }
5488
5489 #[test]
5490 fn test_alias_preserves_explicit_config_in_resolution() {
5491 let registry = CapabilityRegistry::with_builtins();
5492
5493 let configs = vec![AgentCapabilityConfig::with_config(
5494 "virtual_bash".to_string(),
5495 serde_json::json!({"key": "value"}),
5496 )];
5497 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5498 let bash = resolved
5499 .iter()
5500 .find(|c| c.capability_id() == "bashkit_shell")
5501 .expect("alias must resolve to canonical bashkit_shell config");
5502 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5503 }
5504
5505 #[test]
5506 fn test_unregister_by_alias_removes_capability_and_aliases() {
5507 let mut registry = CapabilityRegistry::with_builtins();
5508
5509 assert!(registry.unregister("virtual_bash").is_some());
5510 assert!(!registry.has("bashkit_shell"));
5511 assert!(!registry.has("virtual_bash"));
5512 }
5513
5514 #[test]
5515 fn test_session_storage_capability_features() {
5516 let registry = CapabilityRegistry::with_builtins();
5517
5518 let storage = registry.get("session_storage").unwrap();
5519 let features = storage.features();
5520 assert!(features.contains(&"secrets"));
5521 assert!(features.contains(&"key_value"));
5522 }
5523
5524 #[test]
5525 fn test_session_schedule_capability_features() {
5526 let registry = CapabilityRegistry::with_builtins();
5527
5528 let schedule = registry.get("session_schedule").unwrap();
5529 assert_eq!(schedule.features(), vec!["schedules"]);
5530 }
5531
5532 #[test]
5533 fn test_session_sql_database_capability_features() {
5534 let registry = CapabilityRegistry::with_builtins();
5535
5536 let sql = registry.get("session_sql_database").unwrap();
5537 assert_eq!(sql.features(), vec!["sql_database"]);
5538 }
5539
5540 #[test]
5541 fn test_sample_data_capability_features() {
5542 let registry = CapabilityRegistry::with_builtins();
5543
5544 let sample = registry.get("sample_data").unwrap();
5545 assert_eq!(sample.features(), vec!["file_system"]);
5546 }
5547
5548 #[test]
5549 fn test_compute_features_empty() {
5550 let registry = CapabilityRegistry::with_builtins();
5551
5552 let features = compute_features(&[], ®istry);
5553 assert!(features.is_empty());
5554 }
5555
5556 #[test]
5557 fn test_compute_features_single_capability() {
5558 let registry = CapabilityRegistry::with_builtins();
5559
5560 let features = compute_features(&["session_schedule".to_string()], ®istry);
5561 assert_eq!(features, vec!["schedules"]);
5562 }
5563
5564 #[test]
5565 fn test_compute_features_multiple_capabilities() {
5566 let registry = CapabilityRegistry::with_builtins();
5567
5568 let features = compute_features(
5569 &[
5570 "session_file_system".to_string(),
5571 "session_storage".to_string(),
5572 "session_schedule".to_string(),
5573 ],
5574 ®istry,
5575 );
5576 assert!(features.contains(&"file_system".to_string()));
5577 assert!(features.contains(&"secrets".to_string()));
5578 assert!(features.contains(&"key_value".to_string()));
5579 assert!(features.contains(&"schedules".to_string()));
5580 }
5581
5582 #[test]
5583 fn test_compute_features_deduplicates() {
5584 let registry = CapabilityRegistry::with_builtins();
5585
5586 let features = compute_features(
5588 &[
5589 "session_file_system".to_string(),
5590 "bashkit_shell".to_string(),
5591 ],
5592 ®istry,
5593 );
5594 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5595 assert_eq!(file_system_count, 1, "file_system should appear only once");
5596 }
5597
5598 #[test]
5599 fn test_compute_features_includes_dependency_features() {
5600 let registry = CapabilityRegistry::with_builtins();
5601
5602 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5604 assert!(features.contains(&"file_system".to_string()));
5605 }
5606
5607 #[test]
5608 fn test_compute_features_generic_harness_set() {
5609 let registry = CapabilityRegistry::with_builtins();
5610
5611 let features = compute_features(
5613 &[
5614 "session_file_system".to_string(),
5615 "bashkit_shell".to_string(),
5616 "session_storage".to_string(),
5617 "session".to_string(),
5618 "session_schedule".to_string(),
5619 ],
5620 ®istry,
5621 );
5622 assert!(features.contains(&"file_system".to_string()));
5623 assert!(features.contains(&"secrets".to_string()));
5624 assert!(features.contains(&"key_value".to_string()));
5625 assert!(features.contains(&"schedules".to_string()));
5626 }
5627
5628 #[test]
5629 fn test_compute_features_unknown_capability_ignored() {
5630 let registry = CapabilityRegistry::with_builtins();
5631
5632 let features = compute_features(
5633 &["unknown_cap".to_string(), "session_schedule".to_string()],
5634 ®istry,
5635 );
5636 assert_eq!(features, vec!["schedules"]);
5637 }
5638
5639 #[test]
5640 fn test_risk_level_ordering() {
5641 assert!(RiskLevel::Low < RiskLevel::Medium);
5642 assert!(RiskLevel::Medium < RiskLevel::High);
5643 }
5644
5645 #[test]
5646 fn test_risk_level_serde_roundtrip() {
5647 let high = RiskLevel::High;
5648 let json = serde_json::to_string(&high).unwrap();
5649 assert_eq!(json, "\"high\"");
5650 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5651 assert_eq!(back, RiskLevel::High);
5652 }
5653
5654 #[test]
5655 fn test_capability_risk_levels() {
5656 let registry = CapabilityRegistry::with_builtins();
5657
5658 let bash = registry.get("bashkit_shell").unwrap();
5660 assert_eq!(bash.risk_level(), RiskLevel::High);
5661
5662 let fetch = registry.get("web_fetch").unwrap();
5664 assert_eq!(fetch.risk_level(), RiskLevel::High);
5665
5666 let noop = registry.get("noop").unwrap();
5668 assert_eq!(noop.risk_level(), RiskLevel::Low);
5669 }
5670
5671 #[tokio::test]
5676 async fn test_apply_capabilities_openai_tool_search() {
5677 let registry = CapabilityRegistry::with_builtins();
5678 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5679
5680 let applied = apply_capabilities(
5681 base_runtime_agent.clone(),
5682 &["openai_tool_search".to_string()],
5683 ®istry,
5684 &test_ctx(),
5685 )
5686 .await;
5687
5688 assert_eq!(
5690 applied.runtime_agent.system_prompt,
5691 base_runtime_agent.system_prompt
5692 );
5693 assert!(applied.tool_registry.is_empty());
5694 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
5695
5696 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5698 assert!(ts.enabled);
5699 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5700 }
5701
5702 #[tokio::test]
5703 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
5704 let registry = CapabilityRegistry::with_builtins();
5705 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5706
5707 let applied = apply_capabilities(
5708 base_runtime_agent,
5709 &[
5710 "current_time".to_string(),
5711 "openai_tool_search".to_string(),
5712 "test_math".to_string(),
5713 ],
5714 ®istry,
5715 &test_ctx(),
5716 )
5717 .await;
5718
5719 assert!(applied.tool_registry.has("get_current_time"));
5721 assert!(applied.tool_registry.has("add"));
5722 assert!(applied.tool_registry.has("subtract"));
5723 assert!(applied.tool_registry.has("multiply"));
5724 assert!(applied.tool_registry.has("divide"));
5725
5726 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5728 assert!(ts.enabled);
5729 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5730 }
5731
5732 #[tokio::test]
5733 async fn test_collect_capabilities_tool_search_custom_threshold() {
5734 let registry = CapabilityRegistry::with_builtins();
5735
5736 let configs = vec![AgentCapabilityConfig {
5737 capability_ref: CapabilityId::new("openai_tool_search"),
5738 config: serde_json::json!({"threshold": 5}),
5739 }];
5740
5741 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5742
5743 let ts = collected.tool_search.as_ref().unwrap();
5744 assert!(ts.enabled);
5745 assert_eq!(ts.threshold, 5);
5746 }
5747
5748 #[tokio::test]
5749 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
5750 let registry = CapabilityRegistry::with_builtins();
5751
5752 let configs = vec![
5753 AgentCapabilityConfig {
5754 capability_ref: CapabilityId::new("auto_tool_search"),
5755 config: serde_json::json!({"threshold": 2}),
5756 },
5757 AgentCapabilityConfig {
5758 capability_ref: CapabilityId::new("test_math"),
5759 config: serde_json::json!({}),
5760 },
5761 ];
5762
5763 let ctx = test_ctx().with_model("claude-3-5-haiku");
5767 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5768
5769 assert!(
5770 collected.tool_search.is_none(),
5771 "auto_tool_search must not set a hosted config on a non-native model"
5772 );
5773 assert!(
5774 collected
5775 .tools
5776 .iter()
5777 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5778 "auto_tool_search must contribute the client-side tool_search tool"
5779 );
5780 assert!(
5781 !collected.tool_definition_hooks.is_empty(),
5782 "auto_tool_search must contribute a client-side deferral hook"
5783 );
5784
5785 let mut transformed = collected.tool_definitions.clone();
5786 for hook in &collected.tool_definition_hooks {
5787 transformed = hook.transform(transformed);
5788 }
5789 let add_tool = transformed
5790 .iter()
5791 .find(|tool| tool.name() == "add")
5792 .expect("test_math contributes add");
5793 assert!(
5794 add_tool.parameters().get("properties").is_none(),
5795 "generic auto_tool_search must honor the configured threshold"
5796 );
5797 }
5798
5799 #[tokio::test]
5800 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
5801 let registry = CapabilityRegistry::with_builtins();
5802
5803 let configs = vec![AgentCapabilityConfig {
5804 capability_ref: CapabilityId::new("auto_tool_search"),
5805 config: serde_json::json!({"threshold": 7}),
5806 }];
5807
5808 let ctx = test_ctx().with_model("gpt-5.4");
5811 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5812
5813 let ts = collected
5814 .tool_search
5815 .as_ref()
5816 .expect("auto_tool_search must set a hosted config on a native model");
5817 assert!(ts.enabled);
5818 assert_eq!(ts.threshold, 7);
5819 assert!(
5820 !collected
5821 .tools
5822 .iter()
5823 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5824 "hosted mechanism must not contribute the client-side tool_search tool"
5825 );
5826 assert!(
5827 collected.tool_definition_hooks.is_empty(),
5828 "hosted mechanism must not contribute a client-side deferral hook"
5829 );
5830 }
5831
5832 #[tokio::test]
5833 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
5834 let registry = CapabilityRegistry::with_builtins();
5835
5836 let configs = vec![AgentCapabilityConfig {
5837 capability_ref: CapabilityId::new("auto_tool_search"),
5838 config: serde_json::json!({"threshold": 9}),
5839 }];
5840
5841 let ctx = test_ctx().with_model("claude-opus-4-8");
5844 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5845
5846 let ts = collected
5847 .tool_search
5848 .as_ref()
5849 .expect("auto_tool_search must set a hosted config on a native Claude model");
5850 assert!(ts.enabled);
5851 assert_eq!(ts.threshold, 9);
5852 assert!(
5853 !collected
5854 .tools
5855 .iter()
5856 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5857 "hosted mechanism must not contribute the client-side tool_search tool"
5858 );
5859 assert!(
5860 collected.tool_definition_hooks.is_empty(),
5861 "hosted mechanism must not contribute a client-side deferral hook"
5862 );
5863 }
5864
5865 #[tokio::test]
5866 async fn test_collect_capabilities_no_tool_search_without_capability() {
5867 let registry = CapabilityRegistry::with_builtins();
5868
5869 let configs = vec![AgentCapabilityConfig {
5870 capability_ref: CapabilityId::new("current_time"),
5871 config: serde_json::json!({}),
5872 }];
5873
5874 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5875
5876 assert!(collected.tool_search.is_none());
5877 }
5878
5879 #[tokio::test]
5880 async fn test_collect_capabilities_tool_search_category_propagation() {
5881 let registry = CapabilityRegistry::with_builtins();
5882
5883 let configs = vec![
5885 AgentCapabilityConfig {
5886 capability_ref: CapabilityId::new("test_math"),
5887 config: serde_json::json!({}),
5888 },
5889 AgentCapabilityConfig {
5890 capability_ref: CapabilityId::new("openai_tool_search"),
5891 config: serde_json::json!({}),
5892 },
5893 ];
5894
5895 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5896
5897 assert!(collected.tool_search.is_some());
5899
5900 for tool_def in &collected.tool_definitions {
5902 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
5904 assert!(
5905 tool_def.category().is_some(),
5906 "Tool {} should have a category from its capability",
5907 tool_def.name()
5908 );
5909 }
5910 }
5911 }
5912
5913 #[tokio::test]
5914 async fn test_apply_capabilities_prompt_caching() {
5915 let registry = CapabilityRegistry::with_builtins();
5916 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5917
5918 let applied = apply_capabilities(
5919 base_runtime_agent.clone(),
5920 &["prompt_caching".to_string()],
5921 ®istry,
5922 &test_ctx(),
5923 )
5924 .await;
5925
5926 assert_eq!(
5927 applied.runtime_agent.system_prompt,
5928 base_runtime_agent.system_prompt
5929 );
5930 assert!(applied.tool_registry.is_empty());
5931 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
5932
5933 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
5934 assert!(prompt_cache.enabled);
5935 assert_eq!(
5936 prompt_cache.strategy,
5937 crate::driver_registry::PromptCacheStrategy::Auto
5938 );
5939 assert!(prompt_cache.gemini_cached_content.is_none());
5940 }
5941
5942 #[tokio::test]
5943 async fn test_apply_capabilities_openrouter_server_tools() {
5944 let registry = CapabilityRegistry::with_builtins();
5945 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5946
5947 let configs = vec![AgentCapabilityConfig {
5948 capability_ref: CapabilityId::new("openrouter_server_tools"),
5949 config: serde_json::json!({
5950 "tools": ["web_search", "datetime"],
5951 "web_search_max_results": 4,
5952 }),
5953 }];
5954
5955 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5956 let routing = collected
5957 .openrouter_routing
5958 .as_ref()
5959 .expect("server tools produce routing config");
5960 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
5961 assert_eq!(
5962 kinds,
5963 vec![
5964 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
5965 crate::driver_registry::OpenRouterServerToolKind::Datetime,
5966 ]
5967 );
5968
5969 let applied = apply_capabilities(
5972 base_runtime_agent,
5973 &["openrouter_server_tools".to_string()],
5974 ®istry,
5975 &test_ctx(),
5976 )
5977 .await;
5978 assert!(applied.tool_registry.is_empty());
5979 assert!(applied.runtime_agent.openrouter_routing.is_none());
5980 }
5981
5982 #[tokio::test]
5983 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
5984 let registry = CapabilityRegistry::with_builtins();
5985
5986 let configs = vec![AgentCapabilityConfig {
5987 capability_ref: CapabilityId::new("prompt_caching"),
5988 config: serde_json::json!({"strategy": "auto"}),
5989 }];
5990
5991 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5992
5993 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
5994 assert!(prompt_cache.enabled);
5995 assert_eq!(
5996 prompt_cache.strategy,
5997 crate::driver_registry::PromptCacheStrategy::Auto
5998 );
5999 assert!(prompt_cache.gemini_cached_content.is_none());
6000 }
6001
6002 #[tokio::test]
6003 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6004 let registry = CapabilityRegistry::with_builtins();
6005
6006 let configs = vec![AgentCapabilityConfig {
6007 capability_ref: CapabilityId::new("prompt_caching"),
6008 config: serde_json::json!({
6009 "strategy": "auto",
6010 "gemini_cached_content": "cachedContents/demo-cache"
6011 }),
6012 }];
6013
6014 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6015
6016 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6017 assert_eq!(
6018 prompt_cache.gemini_cached_content.as_deref(),
6019 Some("cachedContents/demo-cache")
6020 );
6021 }
6022
6023 #[tokio::test]
6024 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6025 let registry = CapabilityRegistry::with_builtins();
6026
6027 let collected = collect_capabilities_with_configs(
6029 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6030 ®istry,
6031 &test_ctx(),
6032 )
6033 .await;
6034 assert_eq!(collected.parallel_tool_calls, Some(true));
6035
6036 let collected = collect_capabilities_with_configs(
6038 &[AgentCapabilityConfig {
6039 capability_ref: CapabilityId::new("parallel_tool_calls"),
6040 config: serde_json::json!({"mode": "avoid"}),
6041 }],
6042 ®istry,
6043 &test_ctx(),
6044 )
6045 .await;
6046 assert_eq!(collected.parallel_tool_calls, Some(false));
6047
6048 let collected = collect_capabilities_with_configs(
6050 &[AgentCapabilityConfig {
6051 capability_ref: CapabilityId::new("parallel_tool_calls"),
6052 config: serde_json::json!({"mode": "none"}),
6053 }],
6054 ®istry,
6055 &test_ctx(),
6056 )
6057 .await;
6058 assert_eq!(collected.parallel_tool_calls, None);
6059
6060 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6062 assert_eq!(collected.parallel_tool_calls, None);
6063 }
6064
6065 #[tokio::test]
6066 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6067 let registry = CapabilityRegistry::with_builtins();
6068
6069 let applied = apply_capabilities(
6071 RuntimeAgent::new("p", "gpt-5.2"),
6072 &["parallel_tool_calls".to_string()],
6073 ®istry,
6074 &test_ctx(),
6075 )
6076 .await;
6077 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6078
6079 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6081 base.parallel_tool_calls = Some(false);
6082 let applied = apply_capabilities(
6083 base,
6084 &["parallel_tool_calls".to_string()],
6085 ®istry,
6086 &test_ctx(),
6087 )
6088 .await;
6089 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6090 }
6091
6092 struct SkillContributingCapability;
6097
6098 impl Capability for SkillContributingCapability {
6099 fn id(&self) -> &str {
6100 "contributes_skills"
6101 }
6102 fn name(&self) -> &str {
6103 "Contributes Skills"
6104 }
6105 fn description(&self) -> &str {
6106 "Test capability that contributes skills."
6107 }
6108 fn contribute_skills(&self) -> Vec<SkillContribution> {
6109 vec![
6110 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6111 .with_files(vec![(
6112 "scripts/a.sh".to_string(),
6113 "#!/bin/sh\necho a\n".to_string(),
6114 )]),
6115 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6116 .with_user_invocable(false),
6117 ]
6118 }
6119 }
6120
6121 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6122 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6123 MountSource::InlineFile { content, .. } => content.as_str(),
6124 _ => panic!("Expected InlineFile for SKILL.md"),
6125 }
6126 }
6127
6128 #[tokio::test]
6129 async fn test_contribute_skills_normalized_to_mounts() {
6130 let mut registry = CapabilityRegistry::new();
6131 registry.register(SkillContributingCapability);
6132
6133 let configs = vec![AgentCapabilityConfig {
6134 capability_ref: CapabilityId::new("contributes_skills"),
6135 config: serde_json::json!({}),
6136 }];
6137
6138 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6139
6140 let skill_mounts: Vec<_> = collected
6141 .mounts
6142 .iter()
6143 .filter(|m| m.path.starts_with("/.agents/skills/"))
6144 .collect();
6145 assert_eq!(skill_mounts.len(), 2);
6146
6147 for m in &skill_mounts {
6150 assert!(m.is_readonly());
6151 assert_eq!(m.capability_id, "contributes_skills");
6152 }
6153
6154 let alpha = skill_mounts
6155 .iter()
6156 .find(|m| m.path == "/.agents/skills/alpha-skill")
6157 .expect("alpha-skill mount missing");
6158 match &alpha.source {
6159 MountSource::InlineDirectory { entries } => {
6160 assert!(entries.contains_key("SKILL.md"));
6161 assert!(entries.contains_key("scripts/a.sh"));
6162 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6163 assert_eq!(parsed.name, "alpha-skill");
6164 assert!(parsed.user_invocable);
6165 }
6166 _ => panic!("Expected InlineDirectory"),
6167 }
6168
6169 let beta = skill_mounts
6170 .iter()
6171 .find(|m| m.path == "/.agents/skills/beta-skill")
6172 .expect("beta-skill mount missing");
6173 match &beta.source {
6174 MountSource::InlineDirectory { entries } => {
6175 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6176 assert!(!parsed.user_invocable);
6177 }
6178 _ => panic!("Expected InlineDirectory"),
6179 }
6180 }
6181
6182 #[tokio::test]
6183 async fn test_contribute_skills_default_empty() {
6184 let mut registry = CapabilityRegistry::new();
6187 registry.register(FilterTestCapability { priority: 0 });
6188
6189 let configs = vec![AgentCapabilityConfig {
6190 capability_ref: CapabilityId::new("filter_test"),
6191 config: serde_json::json!({}),
6192 }];
6193
6194 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6195 assert!(
6196 collected
6197 .mounts
6198 .iter()
6199 .all(|m| !m.path.starts_with("/.agents/skills/"))
6200 );
6201 }
6202
6203 struct LocalizedCapability;
6204
6205 impl Capability for LocalizedCapability {
6206 fn id(&self) -> &str {
6207 "localized"
6208 }
6209 fn name(&self) -> &str {
6210 "Localized"
6211 }
6212 fn description(&self) -> &str {
6213 "English description"
6214 }
6215 fn localizations(&self) -> Vec<CapabilityLocalization> {
6216 vec![
6217 CapabilityLocalization {
6218 locale: "en",
6219 name: None,
6220 description: None,
6221 config_description: Some("Controls things."),
6222 config_overlay: None,
6223 },
6224 CapabilityLocalization {
6225 locale: "uk",
6226 name: Some("Локалізована"),
6227 description: Some("Український опис"),
6228 config_description: Some("Керує налаштуваннями."),
6229 config_overlay: None,
6230 },
6231 ]
6232 }
6233 }
6234
6235 #[test]
6236 fn localized_name_falls_back_exact_language_then_base() {
6237 let cap = LocalizedCapability;
6238 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6240 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6241 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6243 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6245 assert_eq!(cap.localized_name(None), "Localized");
6246 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6247 assert_eq!(cap.localized_description(Some("de")), "English description");
6248 }
6249
6250 #[test]
6251 fn describe_schema_resolves_config_description_per_locale() {
6252 let cap = LocalizedCapability;
6253 assert_eq!(
6254 cap.describe_schema(Some("uk-UA")).as_deref(),
6255 Some("Керує налаштуваннями.")
6256 );
6257 assert_eq!(
6259 cap.describe_schema(Some("pl")).as_deref(),
6260 Some("Controls things.")
6261 );
6262 assert_eq!(
6263 cap.describe_schema(None).as_deref(),
6264 Some("Controls things.")
6265 );
6266 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6268 }
6269}