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