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 delegation_result;
103mod error_disclosure;
104pub mod facts;
105mod fake_aws;
106mod fake_crm;
107mod fake_financial;
108mod fake_warehouse;
109mod file_system;
110mod guardrails;
111mod human_intent;
112mod infinity_context;
113mod knowledge_base;
114mod knowledge_index;
115mod loop_detection;
116mod lua;
117mod lua_code_mode;
118pub mod mcp;
119mod memory;
120mod message_metadata;
121mod model_scout;
122mod monitors;
123mod noop;
124mod openai_tool_search;
125mod openrouter_server_tools;
126mod openrouter_workspace;
127#[cfg(feature = "ui-capabilities")]
128mod openui;
129mod parallel_tool_calls;
130mod platform_management;
131mod prompt_caching;
132mod prompt_canary_guardrail;
133mod research;
134mod sample_data;
135mod self_budget;
136mod session;
137mod session_sandbox;
138mod session_schedule;
139mod session_sql_database;
140mod session_storage;
141mod session_tasks;
142mod skills;
143mod skills_scoped;
144mod stateless_todo_list;
145mod subagents;
146mod system_commands;
147mod test_math;
148mod test_weather;
149mod tool_call_repair;
150mod tool_output_distillation;
151mod tool_output_persistence;
152mod tool_search;
153mod usage_limit_auto_continue;
154pub mod user_hooks;
155mod util;
156#[cfg(feature = "web-fetch")]
157mod web_fetch;
158
159pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
164pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
168#[cfg(feature = "a2a")]
169pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
170#[cfg(feature = "ui-capabilities")]
171pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
172pub use agent_handoff::{
173 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
174};
175pub use agent_instructions::{
176 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
177 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
178 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
179};
180pub use attach_skill::{
181 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
182 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
183 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
184};
185pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
186pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
187pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
188pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
189pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
190pub use compaction::{
191 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
192 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
193 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
194 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
195 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
196 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
197 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
198};
199pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
200pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
201pub use declarative::{
202 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
203 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
204 declarative_capability_info, hydrate_declarative_capability_config,
205 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
206 plugin_capability_info, validate_declarative_capability_definition,
207};
208pub use delegation_result::{
209 ReportResultTool, ReportTaskProgressTool, report_result_tool_for_child_session,
210 report_task_progress_tool_for_child_session,
211};
212pub use error_disclosure::{
213 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
214};
215pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
216pub use fake_aws::{
217 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
218 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
219 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
220 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
221};
222pub use fake_crm::{
223 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
224 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
225 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
226};
227pub use fake_financial::{
228 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
229 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
230 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
231 FinanceListTransactionsTool,
232};
233pub use fake_warehouse::{
234 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
235 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
236 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
237 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
238};
239pub use file_system::{
240 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
241 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
242};
243pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
244pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
245pub use infinity_context::{
246 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
247};
248pub use knowledge_base::{
249 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
250 validate_knowledge_base_config,
251};
252pub use knowledge_index::{
253 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
254 validate_knowledge_index_config,
255};
256pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
257pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
258pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
259pub use mcp::{
260 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
261 parse_mcp_capability_id,
262};
263pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
264pub use message_metadata::{
265 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
266 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
267};
268pub use model_scout::{
269 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
270 RouterUpdateProposal, compute_score, rank_results,
271};
272pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
273pub use openai_tool_search::{
274 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
275 model_supports_native_tool_search,
276};
277pub use openrouter_server_tools::{
278 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
279};
280pub use openrouter_workspace::{
281 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
282 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
283 detect_policy_drift,
284};
285#[cfg(feature = "ui-capabilities")]
286pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
287pub use parallel_tool_calls::{
288 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
289 parallel_tool_calls_from_config,
290};
291pub use platform_management::{
292 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
293 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
294 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
295};
296pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
297pub use prompt_canary_guardrail::{
298 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
299 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
300 REASON_CODE_SYSTEM_PROMPT_LEAK,
301};
302pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
303pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
304pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
305pub use session::{
306 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
307};
308pub use session_sandbox::{
309 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
310 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
311};
312pub use session_schedule::{
313 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
314 SessionScheduleCapability,
315};
316pub use session_sql_database::{
317 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
318 SqlSchemaTool,
319};
320pub use session_storage::{
321 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
322 is_internal_session_kv_key,
323};
324pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
325pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
326pub use skills_scoped::{
327 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
328};
329pub use stateless_todo_list::{
330 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
331};
332pub use subagents::{SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool, SubagentCapability};
333pub use usage_limit_auto_continue::{
334 AutoContinueConfig, USAGE_LIMIT_AUTO_CONTINUE_CAPABILITY_ID, UsageLimitAutoContinueCapability,
335 resolve_usage_limit_auto_continue,
336};
337pub use bashkit_shell::{
339 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
340};
341pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
342pub use test_math::{
343 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
344};
345pub use test_weather::{
346 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
347};
348pub use tool_call_repair::{
349 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
350 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
351 salvage_tool_arguments, tool_call_repair_capability,
352};
353pub use tool_output_distillation::{
354 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
355};
356pub use tool_output_persistence::{
357 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
358};
359pub use tool_search::{
360 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
361};
362pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
363#[cfg(feature = "web-fetch")]
364pub use web_fetch::{
365 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
366 derive_bot_auth_public_key,
367};
368
369pub struct SystemPromptContext {
379 pub session_id: SessionId,
381 pub locale: Option<String>,
383 pub file_store: Option<Arc<dyn SessionFileSystem>>,
385 pub model: Option<String>,
391}
392
393impl SystemPromptContext {
394 pub fn without_file_store(session_id: SessionId) -> Self {
396 Self {
397 session_id,
398 locale: None,
399 file_store: None,
400 model: None,
401 }
402 }
403
404 pub fn with_model(mut self, model: impl Into<String>) -> Self {
406 self.model = Some(model.into());
407 self
408 }
409}
410
411#[derive(Debug, Clone)]
463pub struct CapabilityLocalization {
464 pub locale: &'static str,
466 pub name: Option<&'static str>,
468 pub description: Option<&'static str>,
470 pub config_description: Option<&'static str>,
475 pub config_overlay: Option<serde_json::Value>,
481}
482
483impl CapabilityLocalization {
484 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
486 Self {
487 locale,
488 name: Some(name),
489 description: Some(description),
490 config_description: None,
491 config_overlay: None,
492 }
493 }
494}
495
496pub fn resolve_localized_field<T>(
500 localizations: &[CapabilityLocalization],
501 locale: Option<&str>,
502 field: impl Fn(&CapabilityLocalization) -> Option<T>,
503) -> Option<T> {
504 let mut candidates: Vec<String> = Vec::new();
505 if let Some(raw) = locale {
506 let normalized = raw.trim().replace('_', "-").to_lowercase();
507 if !normalized.is_empty() {
508 if let Some((language, _)) = normalized.split_once('-') {
509 let language = language.to_string();
510 candidates.push(normalized);
511 candidates.push(language);
512 } else {
513 candidates.push(normalized);
514 }
515 }
516 }
517 candidates.push("en".to_string());
518
519 for candidate in candidates {
520 let hit = localizations
521 .iter()
522 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
523 .and_then(&field);
524 if hit.is_some() {
525 return hit;
526 }
527 }
528 None
529}
530
531#[async_trait]
532pub trait Capability: Send + Sync {
533 fn id(&self) -> &str;
535
536 fn aliases(&self) -> Vec<&'static str> {
545 vec![]
546 }
547
548 fn name(&self) -> &str;
550
551 fn description(&self) -> &str;
553
554 fn localizations(&self) -> Vec<CapabilityLocalization> {
559 vec![]
560 }
561
562 fn localized_name(&self, locale: Option<&str>) -> String {
565 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
566 .unwrap_or_else(|| self.name())
567 .to_string()
568 }
569
570 fn localized_description(&self, locale: Option<&str>) -> String {
572 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
573 .unwrap_or_else(|| self.description())
574 .to_string()
575 }
576
577 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
581 resolve_localized_field(&self.localizations(), locale, |entry| {
582 entry.config_description
583 })
584 .map(str::to_string)
585 }
586
587 fn status(&self) -> CapabilityStatus {
589 CapabilityStatus::Available
590 }
591
592 fn icon(&self) -> Option<&str> {
594 None
595 }
596
597 fn category(&self) -> Option<&str> {
599 None
600 }
601
602 fn is_guardrail(&self) -> bool {
607 false
608 }
609
610 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
621 None
622 }
623
624 fn system_prompt_addition(&self) -> Option<&str> {
644 None
645 }
646
647 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
659 self.system_prompt_addition().map(|addition| {
660 format!(
661 "<capability id=\"{}\">\n{}\n</capability>",
662 self.id(),
663 addition
664 )
665 })
666 }
667
668 fn system_prompt_preview(&self) -> Option<String> {
674 self.system_prompt_addition().map(|s| s.to_string())
675 }
676
677 fn tools(&self) -> Vec<Box<dyn Tool>> {
679 vec![]
680 }
681
682 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
690 self.tools()
691 }
692
693 async fn system_prompt_contribution_with_config(
700 &self,
701 ctx: &SystemPromptContext,
702 _config: &serde_json::Value,
703 ) -> Option<String> {
704 self.system_prompt_contribution(ctx).await
705 }
706
707 fn tool_definitions(&self) -> Vec<ToolDefinition> {
710 self.tools().iter().map(|t| t.to_definition()).collect()
711 }
712
713 fn mounts(&self) -> Vec<MountPoint> {
721 vec![]
722 }
723
724 fn dependencies(&self) -> Vec<&'static str> {
733 vec![]
734 }
735
736 fn features(&self) -> Vec<&'static str> {
751 vec![]
752 }
753
754 fn config_schema(&self) -> Option<serde_json::Value> {
760 None
761 }
762
763 fn config_ui_schema(&self) -> Option<serde_json::Value> {
768 None
769 }
770
771 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
777 Ok(())
778 }
779
780 fn mcp_servers(&self) -> ScopedMcpServers {
786 ScopedMcpServers::default()
787 }
788
789 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
791 self.mcp_servers()
792 }
793
794 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
807 None
808 }
809
810 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
818 None
819 }
820
821 fn llm_error_hook(&self) -> Option<Arc<dyn crate::llm_error_hook::LlmErrorHook>> {
833 None
834 }
835
836 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
851 vec![]
852 }
853
854 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
865 vec![]
866 }
867
868 fn pre_tool_use_hooks_with_config(
873 &self,
874 _config: &serde_json::Value,
875 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
876 self.pre_tool_use_hooks()
877 }
878
879 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
887 vec![]
888 }
889
890 fn post_tool_exec_hooks_with_config(
895 &self,
896 _config: &serde_json::Value,
897 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
898 self.post_tool_exec_hooks()
899 }
900
901 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
910 vec![]
911 }
912
913 fn tool_definition_hooks_with_config(
918 &self,
919 _config: &serde_json::Value,
920 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
921 self.tool_definition_hooks()
922 }
923
924 fn tool_definition_hooks_with_context(
934 &self,
935 _ctx: &SystemPromptContext,
936 config: &serde_json::Value,
937 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
938 self.tool_definition_hooks_with_config(config)
939 }
940
941 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
949 vec![]
950 }
951
952 fn narrate(
966 &self,
967 _tool_def: Option<&ToolDefinition>,
968 tool_call: &ToolCall,
969 phase: crate::tool_narration::ToolNarrationPhase,
970 locale: Option<&str>,
971 ctx: crate::tool_narration::ToolNarrationContext<'_>,
972 ) -> Option<String> {
973 self.tools()
974 .iter()
975 .find(|tool| tool.name() == tool_call.name)
976 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
977 }
978
979 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
995 vec![]
996 }
997
998 fn user_hooks_with_config(
1004 &self,
1005 _config: &serde_json::Value,
1006 ) -> Vec<crate::user_hook_types::UserHookSpec> {
1007 self.user_hooks()
1008 }
1009
1010 fn risk_level(&self) -> RiskLevel {
1018 RiskLevel::Low
1019 }
1020
1021 fn commands(&self) -> Vec<CommandDescriptor> {
1029 vec![]
1030 }
1031
1032 async fn execute_command(
1046 &self,
1047 request: &ExecuteCommandRequest,
1048 _ctx: &CommandExecutionContext,
1049 ) -> crate::error::Result<CommandResult> {
1050 Err(crate::error::AgentLoopError::config(format!(
1051 "capability {} declared command /{} but does not implement execute_command",
1052 self.id(),
1053 request.name,
1054 )))
1055 }
1056
1057 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1066 vec![]
1067 }
1068
1069 fn contribute_skills(&self) -> Vec<SkillContribution> {
1079 vec![]
1080 }
1081
1082 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1093 vec![]
1094 }
1095
1096 fn post_output_guardrails_with_config(
1108 &self,
1109 _config: &serde_json::Value,
1110 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1111 vec![]
1112 }
1113}
1114
1115pub trait ToolDefinitionHook: Send + Sync {
1116 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1117
1118 fn applies_with_native_tool_search(&self) -> bool {
1123 true
1124 }
1125}
1126
1127pub trait ToolCallHook: Send + Sync {
1128 fn narration(
1129 &self,
1130 _tool_def: Option<&ToolDefinition>,
1131 _tool_call: &ToolCall,
1132 _phase: crate::tool_narration::ToolNarrationPhase,
1133 _locale: Option<&str>,
1134 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
1135 ) -> Option<String> {
1136 None
1137 }
1138
1139 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1140 tool_call
1141 }
1142}
1143
1144pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1150
1151impl ToolCallHook for CapabilityNarrationHook {
1152 fn narration(
1153 &self,
1154 tool_def: Option<&ToolDefinition>,
1155 tool_call: &ToolCall,
1156 phase: crate::tool_narration::ToolNarrationPhase,
1157 locale: Option<&str>,
1158 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1159 ) -> Option<String> {
1160 self.0.narrate(tool_def, tool_call, phase, locale, ctx)
1161 }
1162}
1163
1164#[derive(
1168 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1169)]
1170#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1171#[cfg_attr(feature = "openapi", schema(example = "low"))]
1172#[serde(rename_all = "lowercase")]
1173pub enum RiskLevel {
1174 Low,
1176 Medium,
1178 High,
1180}
1181
1182#[derive(Debug, Clone, Serialize, Deserialize)]
1188#[serde(rename_all = "snake_case")]
1189pub enum BlueprintModel {
1190 Fixed(String),
1192 Default(String),
1194 Inherit,
1196}
1197
1198pub struct AgentBlueprint {
1204 pub id: &'static str,
1206 pub name: &'static str,
1208 pub description: &'static str,
1210 pub model: BlueprintModel,
1212 pub system_prompt: &'static str,
1214 pub tools: Vec<Box<dyn Tool>>,
1216 pub max_turns: Option<usize>,
1218 pub config_schema: Option<serde_json::Value>,
1220}
1221
1222impl AgentBlueprint {
1223 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1225 self.tools.iter().map(|t| t.to_definition()).collect()
1226 }
1227}
1228
1229impl std::fmt::Debug for AgentBlueprint {
1230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1231 f.debug_struct("AgentBlueprint")
1232 .field("id", &self.id)
1233 .field("name", &self.name)
1234 .field("model", &self.model)
1235 .field("tool_count", &self.tools.len())
1236 .field("max_turns", &self.max_turns)
1237 .finish()
1238 }
1239}
1240
1241#[derive(Clone)]
1268pub struct CapabilityRegistry {
1269 capabilities: HashMap<String, Arc<dyn Capability>>,
1270 aliases: HashMap<String, String>,
1272}
1273
1274impl CapabilityRegistry {
1275 pub fn new() -> Self {
1277 Self {
1278 capabilities: HashMap::new(),
1279 aliases: HashMap::new(),
1280 }
1281 }
1282
1283 pub fn with_builtins() -> Self {
1288 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1289 }
1290
1291 pub fn runtime_builtins() -> Self {
1302 let mut registry = Self::new();
1303
1304 registry.register(AgentInstructionsCapability);
1305 registry.register(HumanIntentCapability);
1306 registry.register(NoopCapability);
1307 registry.register(CurrentTimeCapability);
1308 registry.register(MessageMetadataCapability);
1309 registry.register(FileSystemCapability);
1310 registry.register(SessionStorageCapability);
1311 registry.register(SessionCapability);
1312 registry.register(StatelessTodoListCapability);
1313 #[cfg(feature = "web-fetch")]
1314 registry.register(WebFetchCapability::from_env());
1315 registry.register(BashkitShellCapability);
1316 registry.register(BtwCapability);
1317 registry.register(InfinityContextCapability);
1318 registry.register(budgeting::BudgetingCapability);
1319 registry.register(SelfBudgetCapability);
1320 registry.register(CompactionCapability);
1321 registry.register(ErrorDisclosureCapability);
1322 registry.register(OpenAiToolSearchCapability::new());
1323 registry.register(ClaudeToolSearchCapability::new());
1324 registry.register(ToolSearchCapability::new());
1325 registry.register(AutoToolSearchCapability::new());
1326 registry.register(PromptCachingCapability::new());
1327 registry.register(ParallelToolCallsCapability);
1328 registry.register(SkillsCapability);
1329 registry.register(SystemCommandsCapability);
1330 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1331 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1332 registry.register(LoopDetectionCapability);
1333 registry.register(ToolCallRepairCapability);
1334 registry.register(PromptCanaryGuardrailCapability);
1335 registry.register(GuardrailsCapability);
1336 registry.register(user_hooks::UserHooksCapability);
1337
1338 let internal_flags = crate::InternalFeatureFlags::from_env();
1339 if internal_flags.lua {
1340 registry.register(LuaCapability);
1341 registry.register(LuaCodeModeCapability);
1342 }
1343
1344 registry
1345 }
1346
1347 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1352 let mut registry = Self::new();
1353
1354 registry.register(AgentInstructionsCapability);
1356 registry.register(HumanIntentCapability);
1357 registry.register(NoopCapability);
1358 registry.register(CurrentTimeCapability);
1359 registry.register(MessageMetadataCapability);
1360 registry.register(ResearchCapability);
1361 registry.register(ModelScoutCapability);
1362 registry.register(OpenRouterWorkspaceCapability);
1363 registry.register(OpenRouterServerToolsCapability);
1364 registry.register(PlatformManagementCapability);
1365 registry.register(FileSystemCapability);
1366 registry.register(MemoryCapability);
1367 registry.register(SessionStorageCapability);
1368 registry.register(SessionCapability);
1369 registry.register(SessionSqlDatabaseCapability);
1370 registry.register(TestMathCapability);
1371 registry.register(TestWeatherCapability);
1372 registry.register(StatelessTodoListCapability);
1373 #[cfg(feature = "web-fetch")]
1374 registry.register(WebFetchCapability::from_env());
1375 registry.register(BashkitShellCapability);
1376 registry.register(BackgroundExecutionCapability);
1377 registry.register(SessionScheduleCapability);
1378 registry.register(BtwCapability);
1379 registry.register(InfinityContextCapability);
1380 registry.register(budgeting::BudgetingCapability);
1381 registry.register(SelfBudgetCapability);
1382 registry.register(CompactionCapability);
1383 registry.register(ErrorDisclosureCapability);
1384
1385 registry.register(OpenAiToolSearchCapability::new());
1387 registry.register(ClaudeToolSearchCapability::new());
1389 registry.register(ToolSearchCapability::new());
1391 registry.register(AutoToolSearchCapability::new());
1393 registry.register(PromptCachingCapability::new());
1394
1395 registry.register(ParallelToolCallsCapability);
1397
1398 registry.register(SkillsCapability);
1400
1401 registry.register(SubagentCapability);
1403
1404 registry.register(SessionTasksCapability);
1406
1407 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1411 registry.register(AgentHandoffCapability);
1412 #[cfg(feature = "a2a")]
1416 registry.register(A2aAgentDelegationCapability);
1417 }
1418
1419 registry.register(SystemCommandsCapability);
1421
1422 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1424 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1425
1426 registry.register(user_hooks::UserHooksCapability);
1429
1430 registry.register(LoopDetectionCapability);
1432
1433 registry.register(UsageLimitAutoContinueCapability);
1440
1441 registry.register(ToolCallRepairCapability);
1445
1446 registry.register(PromptCanaryGuardrailCapability);
1449
1450 registry.register(GuardrailsCapability);
1453
1454 #[cfg(feature = "ui-capabilities")]
1456 {
1457 registry.register(OpenUiCapability);
1458 registry.register(A2UiCapability);
1459 }
1460
1461 registry.register(SampleDataCapability);
1463
1464 registry.register(DataKnowledgeCapability);
1466
1467 registry.register(KnowledgeBaseCapability);
1469
1470 registry.register(KnowledgeIndexCapability);
1472
1473 registry.register(FakeWarehouseCapability);
1475 registry.register(FakeAwsCapability);
1476 registry.register(FakeCrmCapability);
1477 registry.register(FakeFinancialCapability);
1478
1479 let internal_flags = crate::InternalFeatureFlags::from_env();
1481 if internal_flags.session_sandbox {
1482 registry.register(SessionSandboxCapability);
1483 }
1484
1485 if internal_flags.lua {
1489 registry.register(LuaCapability);
1490 registry.register(LuaCodeModeCapability);
1493 }
1494 for plugin in inventory::iter::<IntegrationPlugin>() {
1495 if (!plugin.experimental_only || grade.experimental_features_enabled())
1496 && plugin
1497 .feature_flag
1498 .is_none_or(|f| internal_flags.is_enabled(f))
1499 {
1500 registry.register_boxed((plugin.factory)());
1501 }
1502 }
1503
1504 registry
1505 }
1506
1507 pub fn register(&mut self, capability: impl Capability + 'static) {
1509 self.register_arc(Arc::new(capability));
1510 }
1511
1512 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1514 self.register_arc(Arc::from(capability));
1515 }
1516
1517 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1519 let canonical = capability.id().to_string();
1520 for alias in capability.aliases() {
1521 self.aliases.insert(alias.to_string(), canonical.clone());
1522 }
1523 self.capabilities.insert(canonical, capability);
1524 }
1525
1526 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1528 self.capabilities
1529 .get(id)
1530 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1531 }
1532
1533 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1538 if self.capabilities.contains_key(id) {
1539 Some(id)
1540 } else {
1541 self.aliases
1542 .get(id)
1543 .filter(|c| self.capabilities.contains_key(*c))
1544 .map(String::as_str)
1545 }
1546 }
1547
1548 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1550 let canonical = self.canonical_id(id)?.to_string();
1551 let removed = self.capabilities.remove(&canonical);
1552 self.aliases.retain(|_, target| *target != canonical);
1553 removed
1554 }
1555
1556 pub fn has(&self, id: &str) -> bool {
1558 self.get(id).is_some()
1559 }
1560
1561 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1563 self.capabilities.values().collect()
1564 }
1565
1566 pub fn len(&self) -> usize {
1568 self.capabilities.len()
1569 }
1570
1571 pub fn is_empty(&self) -> bool {
1573 self.capabilities.is_empty()
1574 }
1575
1576 pub fn builder() -> CapabilityRegistryBuilder {
1578 CapabilityRegistryBuilder::new()
1579 }
1580
1581 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1585 for cap in self.capabilities.values() {
1586 for bp in cap.agent_blueprints() {
1587 if bp.id == id {
1588 return Some(bp);
1589 }
1590 }
1591 }
1592 None
1593 }
1594
1595 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1599 for (capability_id, cap) in &self.capabilities {
1600 for bp in cap.agent_blueprints() {
1601 if bp.id == id {
1602 return Some((capability_id.clone(), bp));
1603 }
1604 }
1605 }
1606 None
1607 }
1608
1609 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1611 self.capabilities
1612 .values()
1613 .flat_map(|cap| cap.agent_blueprints())
1614 .collect()
1615 }
1616}
1617
1618impl Default for CapabilityRegistry {
1619 fn default() -> Self {
1620 Self::with_builtins()
1621 }
1622}
1623
1624impl std::fmt::Debug for CapabilityRegistry {
1625 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1626 let ids: Vec<_> = self.capabilities.keys().collect();
1627 f.debug_struct("CapabilityRegistry")
1628 .field("capabilities", &ids)
1629 .finish()
1630 }
1631}
1632
1633pub struct CapabilityRegistryBuilder {
1635 registry: CapabilityRegistry,
1636}
1637
1638impl CapabilityRegistryBuilder {
1639 pub fn new() -> Self {
1641 Self {
1642 registry: CapabilityRegistry::new(),
1643 }
1644 }
1645
1646 pub fn with_builtins() -> Self {
1648 Self {
1649 registry: CapabilityRegistry::with_builtins(),
1650 }
1651 }
1652
1653 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1655 self.registry.register(capability);
1656 self
1657 }
1658
1659 pub fn build(self) -> CapabilityRegistry {
1661 self.registry
1662 }
1663}
1664
1665impl Default for CapabilityRegistryBuilder {
1666 fn default() -> Self {
1667 Self::new()
1668 }
1669}
1670
1671pub struct ModelViewContext<'a> {
1677 pub session_id: SessionId,
1678 pub prior_usage: Option<&'a TokenUsage>,
1679}
1680
1681pub trait ModelViewProvider: Send + Sync {
1687 fn apply_model_view(
1688 &self,
1689 messages: Vec<Message>,
1690 config: &serde_json::Value,
1691 context: &ModelViewContext<'_>,
1692 ) -> Vec<Message>;
1693
1694 fn priority(&self) -> i32 {
1695 0
1696 }
1697}
1698
1699pub struct CollectedCapabilities {
1704 pub system_prompt_parts: Vec<String>,
1706 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1708 pub tools: Vec<Box<dyn Tool>>,
1710 pub tool_definitions: Vec<ToolDefinition>,
1712 pub mounts: Vec<MountPoint>,
1714 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1716 pub applied_ids: Vec<String>,
1718 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1720 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1722 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1725 pub parallel_tool_calls: Option<bool>,
1729 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1731 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1733 pub mcp_servers: ScopedMcpServers,
1735 }
1741
1742#[derive(Debug, Clone, PartialEq, Eq)]
1743pub struct SystemPromptAttribution {
1744 pub capability_id: String,
1745 pub content: String,
1746}
1747
1748impl CollectedCapabilities {
1749 pub fn system_prompt_prefix(&self) -> Option<String> {
1752 if self.system_prompt_parts.is_empty() {
1753 None
1754 } else {
1755 Some(self.system_prompt_parts.join("\n\n"))
1756 }
1757 }
1758
1759 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1763 for (provider, config) in &self.message_filter_providers {
1765 provider.apply_filters(query, config);
1766 }
1767 }
1768
1769 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1772 for (provider, config) in &self.message_filter_providers {
1773 provider.post_load(messages, config);
1774 }
1775 }
1776
1777 pub fn has_message_filters(&self) -> bool {
1779 !self.message_filter_providers.is_empty()
1780 }
1781}
1782
1783struct SpawnAgentTargetProvider {
1784 target_type: &'static str,
1785 tool: Box<dyn Tool>,
1786}
1787
1788struct UnifiedSpawnAgentTool {
1789 providers: Vec<SpawnAgentTargetProvider>,
1790}
1791
1792impl UnifiedSpawnAgentTool {
1793 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1794 Self { providers }
1795 }
1796
1797 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1798 self.providers
1799 .iter()
1800 .find(|provider| provider.target_type == target_type)
1801 .map(|provider| provider.tool.as_ref())
1802 }
1803
1804 fn target_types(&self) -> Vec<&'static str> {
1805 ["subagent", "agent", "external_a2a"]
1806 .into_iter()
1807 .filter(|target_type| {
1808 self.providers
1809 .iter()
1810 .any(|provider| provider.target_type == *target_type)
1811 })
1812 .collect()
1813 }
1814
1815 fn normalize_arguments(&self, mut arguments: serde_json::Value) -> serde_json::Value {
1816 let target_type = arguments
1817 .get("target")
1818 .and_then(|target| target.get("type"))
1819 .and_then(serde_json::Value::as_str)
1820 .unwrap_or_default();
1821
1822 if target_type == "external_a2a" {
1823 if let Some(target) = arguments
1824 .get_mut("target")
1825 .and_then(serde_json::Value::as_object_mut)
1826 && !target.contains_key("external_agent_id")
1827 && let Some(id) = target.get("id").cloned()
1828 {
1829 target.insert("external_agent_id".to_string(), id);
1830 }
1831 if arguments.get("mode").and_then(serde_json::Value::as_str) == Some("foreground") {
1832 arguments["mode"] = serde_json::Value::String("wait".to_string());
1833 }
1834 } else if matches!(target_type, "subagent" | "agent")
1835 && arguments.get("mode").and_then(serde_json::Value::as_str) == Some("wait")
1836 {
1837 arguments["mode"] = serde_json::Value::String("foreground".to_string());
1838 }
1839
1840 arguments
1841 }
1842}
1843
1844#[async_trait]
1845impl Tool for UnifiedSpawnAgentTool {
1846 fn narrate(
1847 &self,
1848 tool_call: &ToolCall,
1849 phase: crate::tool_narration::ToolNarrationPhase,
1850 locale: Option<&str>,
1851 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1852 ) -> Option<String> {
1853 let target_type = tool_call
1854 .arguments
1855 .get("target")
1856 .and_then(|target| target.get("type"))
1857 .and_then(serde_json::Value::as_str)?;
1858 self.provider_for(target_type)
1859 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1860 }
1861
1862 fn name(&self) -> &str {
1863 "spawn_agent"
1864 }
1865
1866 fn display_name(&self) -> Option<&str> {
1867 Some("Spawn Agent")
1868 }
1869
1870 fn description(&self) -> &str {
1871 "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."
1872 }
1873
1874 fn parameters_schema(&self) -> serde_json::Value {
1875 serde_json::json!({
1876 "type": "object",
1877 "properties": {
1878 "name": {
1879 "type": "string",
1880 "description": "Human-readable name for local subagent or first-party handoff runs. Optional for external A2A targets."
1881 },
1882 "instructions": {
1883 "type": "string",
1884 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
1885 },
1886 "goal": {
1887 "type": "string",
1888 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
1889 },
1890 "lifetime": {
1891 "type": "string",
1892 "enum": ["linked", "detached"],
1893 "default": "linked",
1894 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
1895 },
1896 "seed": {
1897 "type": "string",
1898 "enum": ["fresh", "fork", "workspace"],
1899 "default": "fresh",
1900 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
1901 },
1902 "target": {
1903 "type": "object",
1904 "properties": {
1905 "type": {
1906 "type": "string",
1907 "enum": self.target_types(),
1908 "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."
1909 },
1910 "id": {
1911 "type": "string",
1912 "description": "Configured first-party handoff target id. Also accepted as an alias for external_a2a target.external_agent_id."
1913 },
1914 "external_agent_id": {
1915 "type": "string",
1916 "description": "Configured external A2A agent id."
1917 }
1918 },
1919 "required": ["type"],
1920 "additionalProperties": false
1921 },
1922 "mode": {
1923 "type": "string",
1924 "enum": ["background", "foreground", "wait"],
1925 "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."
1926 },
1927 "blueprint": {
1928 "type": "string",
1929 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
1930 },
1931 "config": {
1932 "type": "object",
1933 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
1934 },
1935 "result_schema": {
1936 "type": "object",
1937 "description": "JSON Schema for a required final structured result. Local child agents must call report_result; external A2A agents must return a structured data artifact."
1938 },
1939 "message_schema": {
1940 "type": "object",
1941 "description": "JSON Schema for structured progress messages from local child agents. When set, the child receives report_task_progress. External A2A targets reject this option explicitly."
1942 },
1943 "public_context": {
1944 "type": "object",
1945 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
1946 },
1947 "wait_timeout_secs": {
1948 "type": "integer",
1949 "minimum": 1,
1950 "maximum": 86400,
1951 "description": "External-A2A-only foreground/wait timeout."
1952 },
1953 "wake_on_completion": {
1954 "type": "boolean",
1955 "description": "External-A2A-only control for background completion wake-ups."
1956 }
1957 },
1958 "required": ["instructions", "target"],
1959 "additionalProperties": false
1960 })
1961 }
1962
1963 fn hints(&self) -> crate::tool_types::ToolHints {
1964 let mut hints = crate::tool_types::ToolHints::default().with_long_running(true);
1965 if self.provider_for("external_a2a").is_some() {
1966 hints = hints.with_open_world(true);
1967 }
1968 hints
1969 }
1970
1971 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
1972 ToolExecutionResult::tool_error(
1973 "spawn_agent requires context. This tool must be executed with session context.",
1974 )
1975 }
1976
1977 async fn execute_with_context(
1978 &self,
1979 arguments: serde_json::Value,
1980 context: &ToolContext,
1981 ) -> ToolExecutionResult {
1982 let target_type = match arguments
1983 .get("target")
1984 .and_then(|target| target.get("type"))
1985 .and_then(serde_json::Value::as_str)
1986 {
1987 Some(target_type) => target_type,
1988 None => {
1989 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
1990 }
1991 };
1992
1993 let Some(provider) = self.provider_for(target_type) else {
1994 let supported = self.target_types().join(", ");
1995 return ToolExecutionResult::tool_error(format!(
1996 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
1997 ));
1998 };
1999 if target_type == "external_a2a"
2000 && arguments
2001 .get("lifetime")
2002 .and_then(serde_json::Value::as_str)
2003 .is_some_and(|value| value == "detached")
2004 {
2005 return ToolExecutionResult::tool_error(
2006 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2007 );
2008 }
2009 if target_type == "external_a2a"
2010 && arguments
2011 .get("message_schema")
2012 .is_some_and(|schema| !schema.is_null())
2013 {
2014 return ToolExecutionResult::tool_error(
2015 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2016 );
2017 }
2018
2019 provider
2020 .execute_with_context(self.normalize_arguments(arguments), context)
2021 .await
2022 }
2023
2024 fn requires_context(&self) -> bool {
2025 true
2026 }
2027}
2028
2029pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2034 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2035 return base_system_prompt.to_string();
2036 };
2037
2038 if base_system_prompt.is_empty() {
2039 return additions.to_string();
2040 }
2041
2042 if base_system_prompt.contains("<system-prompt>") {
2043 format!("{base_system_prompt}\n\n{additions}")
2044 } else {
2045 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2046 }
2047}
2048
2049pub struct CollectedMessageFilters {
2056 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2058}
2059
2060pub struct CollectedModelViewProviders {
2062 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2064}
2065
2066impl CollectedMessageFilters {
2072 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2074 for (provider, config) in &self.message_filter_providers {
2075 provider.apply_filters(query, config);
2076 }
2077 }
2078
2079 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2081 for (provider, config) in &self.message_filter_providers {
2082 provider.post_load(messages, config);
2083 }
2084 }
2085}
2086
2087impl CollectedModelViewProviders {
2088 pub fn apply_model_view(
2090 &self,
2091 mut messages: Vec<Message>,
2092 context: &ModelViewContext<'_>,
2093 ) -> Vec<Message> {
2094 for (provider, config) in &self.model_view_providers {
2095 messages = provider.apply_model_view(messages, config, context);
2096 }
2097 messages
2098 }
2099}
2100
2101fn compaction_is_enabled(
2107 capability_configs: &[AgentCapabilityConfig],
2108 registry: &CapabilityRegistry,
2109) -> bool {
2110 capability_configs.iter().any(|cap_config| {
2111 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2112 && registry
2113 .get(cap_config.capability_ref.as_str())
2114 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2115 })
2116}
2117
2118fn message_filter_config_for(
2127 cap_id: &str,
2128 base: &serde_json::Value,
2129 compaction_on: bool,
2130) -> serde_json::Value {
2131 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2132 return base.clone();
2133 }
2134 let mut config = base.clone();
2135 match config.as_object_mut() {
2136 Some(map) => {
2137 map.insert(
2138 "compaction_active".to_string(),
2139 serde_json::Value::Bool(true),
2140 );
2141 }
2142 None => {
2143 config = serde_json::json!({ "compaction_active": true });
2144 }
2145 }
2146 config
2147}
2148
2149pub fn collect_message_filters_only(
2155 capability_configs: &[AgentCapabilityConfig],
2156 registry: &CapabilityRegistry,
2157) -> CollectedMessageFilters {
2158 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2159 Vec::new();
2160 let compaction_on = compaction_is_enabled(capability_configs, registry);
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
2171 .resolve_for_model(None)
2172 .unwrap_or_else(|| capability.as_ref());
2173 if let Some(provider) = effective.message_filter_provider() {
2174 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2175 message_filter_providers.push((provider, config));
2176 }
2177 }
2178 }
2179
2180 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2181
2182 CollectedMessageFilters {
2183 message_filter_providers,
2184 }
2185}
2186
2187pub fn collect_model_view_providers(
2194 capability_configs: &[AgentCapabilityConfig],
2195 registry: &CapabilityRegistry,
2196 model: Option<&str>,
2197) -> CollectedModelViewProviders {
2198 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2199
2200 for cap_config in capability_configs {
2201 let cap_id = cap_config.capability_ref.as_str();
2202 if let Some(capability) = registry.get(cap_id) {
2203 if capability.status() != CapabilityStatus::Available {
2204 continue;
2205 }
2206 let effective: &dyn Capability = capability
2207 .resolve_for_model(model)
2208 .unwrap_or_else(|| capability.as_ref());
2209 if let Some(provider) = effective.model_view_provider() {
2210 model_view_providers.push((provider, cap_config.config.clone()));
2211 }
2212 }
2213 }
2214
2215 model_view_providers.sort_by_key(|(p, _)| p.priority());
2216
2217 CollectedModelViewProviders {
2218 model_view_providers,
2219 }
2220}
2221
2222pub fn collect_dynamic_facts(
2228 capability_configs: &[AgentCapabilityConfig],
2229 registry: &CapabilityRegistry,
2230 model: Option<&str>,
2231 ctx: &FactsContext,
2232) -> Vec<Fact> {
2233 let mut dynamic = Vec::new();
2234 for cap_config in capability_configs {
2235 let cap_id = cap_config.capability_ref.as_str();
2236 if let Some(capability) = registry.get(cap_id) {
2237 if capability.status() != CapabilityStatus::Available {
2238 continue;
2239 }
2240 let effective: &dyn Capability = capability
2241 .resolve_for_model(model)
2242 .unwrap_or_else(|| capability.as_ref());
2243 for fact in effective.facts(&cap_config.config, ctx) {
2244 if fact.volatility == Volatility::Dynamic {
2245 dynamic.push(fact);
2246 }
2247 }
2248 }
2249 }
2250 dynamic
2251}
2252
2253pub fn collect_capability_mcp_servers(
2254 capability_configs: &[AgentCapabilityConfig],
2255 registry: &CapabilityRegistry,
2256) -> ScopedMcpServers {
2257 let mut servers = ScopedMcpServers::default();
2258
2259 for cap_config in capability_configs {
2260 let cap_id = cap_config.capability_ref.as_str();
2261 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2264 if let Ok(definition) =
2265 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2266 {
2267 if definition.status != CapabilityStatus::Available {
2268 continue;
2269 }
2270 if let Some(contributed) = definition.mcp_servers {
2271 servers = merge_scoped_mcp_servers(&servers, &contributed);
2272 }
2273 }
2274 continue;
2275 }
2276 if let Some(capability) = registry.get(cap_id) {
2277 if capability.status() != CapabilityStatus::Available {
2278 continue;
2279 }
2280 servers = merge_scoped_mcp_servers(
2281 &servers,
2282 &capability.mcp_servers_with_config(&cap_config.config),
2283 );
2284 }
2285 }
2286
2287 servers
2288}
2289
2290pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2297
2298#[derive(Debug, Clone, PartialEq, Eq)]
2300pub enum DependencyError {
2301 CircularDependency {
2303 capability_id: String,
2305 chain: Vec<String>,
2307 },
2308 TooManyCapabilities {
2310 count: usize,
2312 max: usize,
2314 },
2315}
2316
2317impl std::fmt::Display for DependencyError {
2318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2319 match self {
2320 DependencyError::CircularDependency {
2321 capability_id,
2322 chain,
2323 } => {
2324 write!(
2325 f,
2326 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2327 capability_id,
2328 chain.join(" -> "),
2329 capability_id
2330 )
2331 }
2332 DependencyError::TooManyCapabilities { count, max } => {
2333 write!(
2334 f,
2335 "Too many capabilities after resolution: {} (max: {})",
2336 count, max
2337 )
2338 }
2339 }
2340 }
2341}
2342
2343impl std::error::Error for DependencyError {}
2344
2345#[derive(Debug, Clone)]
2347pub struct ResolvedCapabilities {
2348 pub resolved_ids: Vec<String>,
2351 pub added_as_dependencies: Vec<String>,
2353 pub user_selected: Vec<String>,
2355}
2356
2357pub fn resolve_dependencies(
2377 selected_ids: &[String],
2378 registry: &CapabilityRegistry,
2379) -> Result<ResolvedCapabilities, DependencyError> {
2380 use std::collections::HashSet;
2381
2382 let user_selected: HashSet<String> = selected_ids
2384 .iter()
2385 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2386 .collect();
2387 let mut resolved: Vec<String> = Vec::new();
2388 let mut resolved_set: HashSet<String> = HashSet::new();
2389 let mut added_as_dependencies: Vec<String> = Vec::new();
2390
2391 for cap_id in selected_ids {
2393 resolve_single_capability(
2394 cap_id,
2395 registry,
2396 &mut resolved,
2397 &mut resolved_set,
2398 &mut added_as_dependencies,
2399 &user_selected,
2400 &mut Vec::new(), )?;
2402 }
2403
2404 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2406 return Err(DependencyError::TooManyCapabilities {
2407 count: resolved.len(),
2408 max: MAX_RESOLVED_CAPABILITIES,
2409 });
2410 }
2411
2412 Ok(ResolvedCapabilities {
2413 resolved_ids: resolved,
2414 added_as_dependencies,
2415 user_selected: selected_ids.to_vec(),
2416 })
2417}
2418
2419pub fn resolve_capability_configs(
2424 selected_configs: &[AgentCapabilityConfig],
2425 registry: &CapabilityRegistry,
2426) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2427 let mut selected_ids: Vec<String> = Vec::new();
2428 for config in selected_configs {
2429 if (is_declarative_capability(config.capability_id())
2432 || is_plugin_capability(config.capability_id()))
2433 && let Ok(definition) =
2434 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2435 {
2436 selected_ids.extend(definition.dependencies);
2437 }
2438 selected_ids.push(config.capability_id().to_string());
2439 }
2440 let resolved = resolve_dependencies(&selected_ids, registry)?;
2441
2442 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2445 .iter()
2446 .map(|config| {
2447 let id = config.capability_id();
2448 let id = registry.canonical_id(id).unwrap_or(id);
2449 (id.to_string(), config.config.clone())
2450 })
2451 .collect();
2452
2453 Ok(resolved
2454 .resolved_ids
2455 .into_iter()
2456 .map(|capability_id| {
2457 explicit_configs
2458 .get(&capability_id)
2459 .cloned()
2460 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2461 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2462 })
2463 .collect())
2464}
2465
2466fn resolve_single_capability(
2468 cap_id: &str,
2469 registry: &CapabilityRegistry,
2470 resolved: &mut Vec<String>,
2471 resolved_set: &mut std::collections::HashSet<String>,
2472 added_as_dependencies: &mut Vec<String>,
2473 user_selected: &std::collections::HashSet<String>,
2474 visiting: &mut Vec<String>,
2475) -> Result<(), DependencyError> {
2476 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2480
2481 if resolved_set.contains(cap_id) {
2483 return Ok(());
2484 }
2485
2486 if visiting.contains(&cap_id.to_string()) {
2488 return Err(DependencyError::CircularDependency {
2489 capability_id: cap_id.to_string(),
2490 chain: visiting.clone(),
2491 });
2492 }
2493
2494 let capability = match registry.get(cap_id) {
2496 Some(cap) => cap,
2497 None => {
2498 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2502 && !resolved_set.contains(cap_id)
2503 {
2504 resolved.push(cap_id.to_string());
2505 resolved_set.insert(cap_id.to_string());
2506 if !user_selected.contains(cap_id) {
2507 added_as_dependencies.push(cap_id.to_string());
2508 }
2509 }
2510 return Ok(());
2511 }
2512 };
2513
2514 visiting.push(cap_id.to_string());
2516
2517 for dep_id in capability.dependencies() {
2519 resolve_single_capability(
2520 dep_id,
2521 registry,
2522 resolved,
2523 resolved_set,
2524 added_as_dependencies,
2525 user_selected,
2526 visiting,
2527 )?;
2528 }
2529
2530 visiting.pop();
2532
2533 if !resolved_set.contains(cap_id) {
2535 resolved.push(cap_id.to_string());
2536 resolved_set.insert(cap_id.to_string());
2537
2538 if !user_selected.contains(cap_id) {
2540 added_as_dependencies.push(cap_id.to_string());
2541 }
2542 }
2543
2544 Ok(())
2545}
2546
2547pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2552 use std::collections::HashSet;
2553
2554 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2555 Ok(resolved) => resolved.resolved_ids,
2556 Err(_) => capability_ids.to_vec(),
2557 };
2558
2559 let mut seen = HashSet::new();
2560 let mut features = Vec::new();
2561 for cap_id in &resolved_ids {
2562 if let Some(cap) = registry.get(cap_id) {
2563 for feature in cap.features() {
2564 if seen.insert(feature) {
2565 features.push(feature.to_string());
2566 }
2567 }
2568 }
2569 }
2570 features
2571}
2572
2573pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2576 registry
2577 .get(cap_id)
2578 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2579 .unwrap_or_default()
2580}
2581
2582pub async fn collect_capabilities(
2598 capability_ids: &[String],
2599 registry: &CapabilityRegistry,
2600 ctx: &SystemPromptContext,
2601) -> CollectedCapabilities {
2602 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2605 Ok(resolved) => resolved.resolved_ids,
2606 Err(e) => {
2607 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2608 capability_ids.to_vec()
2609 }
2610 };
2611
2612 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2614 .iter()
2615 .map(|id| AgentCapabilityConfig {
2616 capability_ref: CapabilityId::new(id),
2617 config: serde_json::Value::Object(serde_json::Map::new()),
2618 })
2619 .collect();
2620
2621 collect_capabilities_with_configs(&configs, registry, ctx).await
2622}
2623
2624pub async fn collect_capabilities_with_configs(
2635 capability_configs: &[AgentCapabilityConfig],
2636 registry: &CapabilityRegistry,
2637 ctx: &SystemPromptContext,
2638) -> CollectedCapabilities {
2639 let mut system_prompt_parts: Vec<String> = Vec::new();
2640 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2641 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2642 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2643 let mut mounts: Vec<MountPoint> = Vec::new();
2644 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2645 Vec::new();
2646 let mut applied_ids: Vec<String> = Vec::new();
2647 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2648 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2649 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2650 let mut parallel_tool_calls: Option<bool> = None;
2651 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2652 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2653 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2656 let mut mcp_servers = ScopedMcpServers::default();
2657 let mut static_facts: Vec<Fact> = Vec::new();
2661 let mut has_dynamic_facts = false;
2662 let facts_ctx = FactsContext::new(ctx.session_id);
2663 let compaction_on = compaction_is_enabled(capability_configs, registry);
2664 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2665 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2666
2667 for cap_config in capability_configs {
2668 let cap_id = cap_config.capability_ref.as_str();
2669 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2674 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2675 cap_config.config.clone(),
2676 ) {
2677 Ok(definition) => {
2678 if definition.status != CapabilityStatus::Available {
2679 continue;
2680 }
2681
2682 if let Some(prompt) = definition.system_prompt.as_deref() {
2683 let contribution =
2684 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2685 system_prompt_attributions.push(SystemPromptAttribution {
2686 capability_id: cap_id.to_string(),
2687 content: contribution.clone(),
2688 });
2689 system_prompt_parts.push(contribution);
2690 }
2691
2692 mounts.extend(definition.mounts(cap_id));
2693 if let Some(ref servers) = definition.mcp_servers {
2694 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2695 }
2696 for skill in definition.skill_contributions() {
2697 mounts.push(skill.to_mount(cap_id));
2698 }
2699
2700 applied_ids.push(cap_id.to_string());
2701 }
2702 Err(error) => {
2703 tracing::warn!(
2704 capability_id = %cap_id,
2705 error = %error,
2706 "Skipping invalid declarative/plugin capability config"
2707 );
2708 }
2709 }
2710 continue;
2711 }
2712 if let Some(capability) = registry.get(cap_id) {
2713 if capability.status() != CapabilityStatus::Available {
2715 continue;
2716 }
2717
2718 let effective: &dyn Capability =
2730 match capability.resolve_for_model(ctx.model.as_deref()) {
2731 Some(inner) => inner,
2732 None => capability.as_ref(),
2733 };
2734 let effective_id = effective.id();
2735 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2736 agent_handoff_spawn_config = Some(cap_config.config.clone());
2737 }
2738
2739 if let Some(contribution) = effective
2741 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2742 .await
2743 {
2744 system_prompt_attributions.push(SystemPromptAttribution {
2745 capability_id: cap_id.to_string(),
2746 content: contribution.clone(),
2747 });
2748 system_prompt_parts.push(contribution);
2749 }
2750
2751 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2756 match fact.volatility {
2757 Volatility::Static => static_facts.push(fact),
2758 Volatility::Dynamic => has_dynamic_facts = true,
2759 }
2760 }
2761
2762 for tool in effective.tools_with_config(&cap_config.config) {
2764 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2765 spawn_agent_providers.push(SpawnAgentTargetProvider {
2766 target_type: "external_a2a",
2767 tool,
2768 });
2769 } else {
2770 tools.push(tool);
2771 }
2772 }
2773 tool_definition_hooks
2774 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2775 tool_call_hooks.extend(effective.tool_call_hooks());
2776 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2778 let cap_category = effective.category();
2783 for def in effective.tool_definitions() {
2784 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2785 continue;
2786 }
2787 let def = match (def.category(), cap_category) {
2788 (None, Some(cat)) => def.with_category(cat),
2789 _ => def,
2790 }
2791 .with_capability_attribution(cap_id, Some(capability.name()));
2792 tool_definitions.push(def);
2793 }
2794
2795 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2803 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2804 {
2805 let threshold = cap_config
2807 .config
2808 .get("threshold")
2809 .and_then(|v| v.as_u64())
2810 .map(|v| v as usize)
2811 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2812 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2813 enabled: true,
2814 threshold,
2815 });
2816 }
2817
2818 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2819 let strategy = cap_config
2820 .config
2821 .get("strategy")
2822 .and_then(|v| v.as_str())
2823 .map(|value| match value {
2824 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2825 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2826 })
2827 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2828 let gemini_cached_content = cap_config
2829 .config
2830 .get("gemini_cached_content")
2831 .and_then(|v| v.as_str())
2832 .map(str::to_string);
2833 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2834 enabled: true,
2835 strategy,
2836 gemini_cached_content,
2837 });
2838 }
2839
2840 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2841 parallel_tool_calls =
2842 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2843 }
2844
2845 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2846 let server_tools =
2847 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2848 if !server_tools.is_empty() {
2849 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2850 server_tools,
2851 ..Default::default()
2852 });
2853 }
2854 }
2855
2856 mounts.extend(effective.mounts());
2858
2859 mcp_servers = merge_scoped_mcp_servers(
2860 &mcp_servers,
2861 &effective.mcp_servers_with_config(&cap_config.config),
2862 );
2863
2864 for skill in effective.contribute_skills() {
2868 mounts.push(skill.to_mount(cap_id));
2869 }
2870
2871 if let Some(provider) = effective.message_filter_provider() {
2873 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2874 message_filter_providers.push((provider, config));
2875 }
2876
2877 applied_ids.push(cap_id.to_string());
2878 }
2879 }
2880
2881 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
2886 spawn_agent_providers.push(SpawnAgentTargetProvider {
2887 target_type: "subagent",
2888 tool: Box::new(SpawnSubagentAsAgentTool),
2889 });
2890 }
2891 if let Some(config) = agent_handoff_spawn_config.as_ref() {
2892 spawn_agent_providers.push(SpawnAgentTargetProvider {
2893 target_type: "agent",
2894 tool: Box::new(SpawnAgentHandoffTool::new(config)),
2895 });
2896 }
2897 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
2898 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
2899 let def = tool
2900 .to_definition()
2901 .with_category("Orchestration")
2902 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
2903 tools.push(Box::new(tool));
2904 tool_definitions.push(def);
2905 }
2906
2907 if !applied_ids
2919 .iter()
2920 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2921 && tool_definitions
2922 .iter()
2923 .any(|def| def.hints().supports_background == Some(true))
2924 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2925 && bg_cap.status() == CapabilityStatus::Available
2926 {
2927 tools.extend(bg_cap.tools());
2928 let cap_category = bg_cap.category();
2929 for def in bg_cap.tool_definitions() {
2930 let def = match (def.category(), cap_category) {
2931 (None, Some(cat)) => def.with_category(cat),
2932 _ => def,
2933 }
2934 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2935 tool_definitions.push(def);
2936 }
2937 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2938 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2939 }
2940
2941 if let Some(block) = facts::render_facts_block(&static_facts) {
2946 system_prompt_attributions.push(SystemPromptAttribution {
2947 capability_id: "facts".to_string(),
2948 content: block.clone(),
2949 });
2950 system_prompt_parts.push(block);
2951 }
2952 if has_dynamic_facts {
2953 system_prompt_attributions.push(SystemPromptAttribution {
2954 capability_id: "facts".to_string(),
2955 content: FACTS_DYNAMIC_NOTE.to_string(),
2956 });
2957 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
2958 }
2959
2960 tool_call_hooks.extend(narration_hooks);
2964
2965 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2967
2968 CollectedCapabilities {
2969 system_prompt_parts,
2970 system_prompt_attributions,
2971 tools,
2972 tool_definitions,
2973 mounts,
2974 message_filter_providers,
2975 applied_ids,
2976 tool_search,
2977 prompt_cache,
2978 openrouter_routing,
2979 parallel_tool_calls,
2980 tool_definition_hooks,
2981 tool_call_hooks,
2982 mcp_servers,
2983 }
2984}
2985
2986pub struct AppliedCapabilities {
2992 pub runtime_agent: RuntimeAgent,
2994 pub tool_registry: ToolRegistry,
2996 pub applied_ids: Vec<String>,
2998}
2999
3000pub async fn apply_capabilities(
3037 base_runtime_agent: RuntimeAgent,
3038 capability_ids: &[String],
3039 registry: &CapabilityRegistry,
3040 ctx: &SystemPromptContext,
3041) -> AppliedCapabilities {
3042 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3043
3044 let final_system_prompt = compose_system_prompt(
3046 &base_runtime_agent.system_prompt,
3047 collected.system_prompt_prefix().as_deref(),
3048 );
3049
3050 let mut tool_registry = ToolRegistry::new();
3052 for tool in collected.tools {
3053 tool_registry.register_boxed(tool);
3054 }
3055
3056 let mut tools = collected.tool_definitions;
3058 for hook in &collected.tool_definition_hooks {
3059 tools = hook.transform(tools);
3060 }
3061
3062 let runtime_agent = RuntimeAgent {
3063 system_prompt: final_system_prompt,
3064 model: base_runtime_agent.model,
3065 tools,
3066 max_iterations: base_runtime_agent.max_iterations,
3067 temperature: base_runtime_agent.temperature,
3068 max_tokens: base_runtime_agent.max_tokens,
3069 tool_search: collected.tool_search,
3070 prompt_cache: collected.prompt_cache,
3071 openrouter_routing: collected.openrouter_routing,
3072 network_access: base_runtime_agent.network_access,
3073 parallel_tool_calls: base_runtime_agent
3076 .parallel_tool_calls
3077 .or(collected.parallel_tool_calls),
3078 };
3079
3080 AppliedCapabilities {
3081 runtime_agent,
3082 tool_registry,
3083 applied_ids: collected.applied_ids,
3084 }
3085}
3086
3087#[cfg(test)]
3092mod tests {
3093 use super::*;
3094 use crate::typed_id::SessionId;
3095 use std::collections::BTreeSet;
3096 use uuid::Uuid;
3097
3098 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3100
3101 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3102 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3103 }
3104
3105 fn test_ctx() -> SystemPromptContext {
3107 SystemPromptContext::without_file_store(SessionId::new())
3108 }
3109
3110 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3112 let mut ids = [
3113 "agent_instructions",
3114 "human_intent",
3115 "budgeting",
3116 "self_budget",
3117 "noop",
3118 "current_time",
3119 "research",
3120 "platform_management",
3121 "session_file_system",
3122 "session_storage",
3123 "session",
3124 "session_sql_database",
3125 "test_math",
3126 "test_weather",
3127 "stateless_todo_list",
3128 "web_fetch",
3129 "bashkit_shell",
3130 "background_execution",
3131 "session_schedule",
3132 "btw",
3133 "infinity_context",
3134 "compaction",
3135 "memory",
3136 "message_metadata",
3137 "openai_tool_search",
3138 "claude_tool_search",
3139 "tool_search",
3140 "auto_tool_search",
3141 "prompt_caching",
3142 "parallel_tool_calls",
3143 "session_tasks",
3144 "skills",
3145 "subagents",
3146 "system_commands",
3147 "sample_data",
3148 "data_knowledge",
3149 "knowledge_base",
3150 "knowledge_index",
3151 "tool_output_persistence",
3152 "tool_output_distillation",
3153 "fake_warehouse",
3154 "fake_aws",
3155 "fake_crm",
3156 "fake_financial",
3157 "loop_detection",
3158 "usage_limit_auto_continue",
3159 "tool_call_repair",
3160 "error_disclosure",
3161 "prompt_canary_guardrail",
3162 "guardrails",
3163 "user_hooks",
3164 "model_scout",
3165 "openrouter_workspace",
3166 "openrouter_server_tools",
3167 ]
3168 .into_iter()
3169 .collect::<BTreeSet<_>>();
3170 if cfg!(feature = "ui-capabilities") {
3171 ids.insert("openui");
3172 ids.insert("a2ui");
3173 }
3174 ids
3175 }
3176
3177 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3179 let mut ids = [
3180 "agent_instructions",
3181 "human_intent",
3182 "budgeting",
3183 "self_budget",
3184 "noop",
3185 "current_time",
3186 "session_file_system",
3187 "session_storage",
3188 "session",
3189 "stateless_todo_list",
3190 "bashkit_shell",
3191 "btw",
3192 "infinity_context",
3193 "compaction",
3194 "message_metadata",
3195 "openai_tool_search",
3196 "claude_tool_search",
3197 "tool_search",
3198 "auto_tool_search",
3199 "prompt_caching",
3200 "parallel_tool_calls",
3201 "skills",
3202 "system_commands",
3203 "tool_output_persistence",
3204 "tool_output_distillation",
3205 "loop_detection",
3206 "tool_call_repair",
3207 "error_disclosure",
3208 "prompt_canary_guardrail",
3209 "guardrails",
3210 "user_hooks",
3211 ]
3212 .into_iter()
3213 .collect::<BTreeSet<_>>();
3214 if cfg!(feature = "web-fetch") {
3215 ids.insert("web_fetch");
3216 }
3217 ids
3218 }
3219
3220 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3222 let mut ids = expected_core_builtin_ids();
3223 ids.insert("agent_handoff");
3224 ids.insert("a2a_agent_delegation");
3225 ids
3226 }
3227
3228 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3229 registry.capabilities.keys().map(String::as_str).collect()
3230 }
3231
3232 #[test]
3242 fn test_capability_registry_with_builtins_dev() {
3243 let _lock = lock_env();
3245 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3246 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3247 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3248 assert!(registry.has("agent_handoff"));
3249 assert!(registry.has("a2a_agent_delegation"));
3250 }
3251
3252 #[test]
3253 fn test_capability_registry_with_builtins_prod() {
3254 let _lock = lock_env();
3256 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3257 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3258 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3259 assert!(!registry.has("docker_container"));
3261 assert!(!registry.has("agent_handoff"));
3262 assert!(!registry.has("a2a_agent_delegation"));
3263 }
3264
3265 #[test]
3266 fn test_capability_registry_runtime_builtins() {
3267 let _lock = lock_env();
3268 unsafe { std::env::remove_var("FEATURE_LUA") };
3269 let registry = CapabilityRegistry::runtime_builtins();
3270 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3271 assert!(registry.has("session_file_system"));
3272 #[cfg(feature = "web-fetch")]
3273 assert!(registry.has("web_fetch"));
3274 assert!(registry.has("bashkit_shell"));
3275
3276 for platform_only in [
3277 "platform_management",
3278 "model_scout",
3279 "openrouter_workspace",
3280 "openrouter_server_tools",
3281 "session_tasks",
3282 "session_schedule",
3283 "subagents",
3284 "background_execution",
3285 "session_sql_database",
3286 "knowledge_base",
3287 "knowledge_index",
3288 "sample_data",
3289 "data_knowledge",
3290 "fake_aws",
3291 "fake_crm",
3292 "fake_financial",
3293 "fake_warehouse",
3294 "test_math",
3295 "test_weather",
3296 "research",
3297 ] {
3298 assert!(
3299 !registry.has(platform_only),
3300 "`{platform_only}` should not be in the runtime default registry"
3301 );
3302 }
3303 }
3304
3305 #[test]
3306 fn test_agent_delegation_enabled_by_env_in_prod() {
3307 let _lock = lock_env();
3309 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3310 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3311 assert!(registry.has("agent_handoff"));
3312 assert!(registry.has("a2a_agent_delegation"));
3313 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3314 }
3315
3316 #[test]
3317 fn test_agent_delegation_disabled_by_env_in_dev() {
3318 let _lock = lock_env();
3320 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3321 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3322 assert!(!registry.has("agent_handoff"));
3323 assert!(!registry.has("a2a_agent_delegation"));
3324 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3325 }
3326
3327 #[test]
3328 fn test_capability_registry_get() {
3329 let registry = CapabilityRegistry::with_builtins();
3330
3331 let noop = registry.get("noop").unwrap();
3332 assert_eq!(noop.id(), "noop");
3333 assert_eq!(noop.name(), "No-Op");
3334 assert_eq!(noop.status(), CapabilityStatus::Available);
3335 }
3336
3337 #[test]
3345 fn builtin_capabilities_satisfy_registry_invariants() {
3346 let registry = CapabilityRegistry::with_builtins();
3347
3348 for cap in registry.list() {
3349 let id = cap.id();
3350 assert!(!id.is_empty(), "capability has an empty id");
3351 assert!(
3352 !cap.name().trim().is_empty(),
3353 "capability `{id}` has an empty name"
3354 );
3355
3356 assert!(
3359 registry.get(id).is_some(),
3360 "capability `{id}` does not resolve by its own id"
3361 );
3362
3363 for dep in cap.dependencies() {
3367 assert!(
3368 registry.get(dep).is_some(),
3369 "capability `{id}` depends on `{dep}`, which is not registered"
3370 );
3371 }
3372
3373 let mut seen = std::collections::HashSet::new();
3376 for tool in cap.tools() {
3377 let name = tool.name().to_string();
3378 assert!(
3379 !name.is_empty(),
3380 "capability `{id}` exposes a tool with an empty name"
3381 );
3382 assert!(
3383 seen.insert(name.clone()),
3384 "capability `{id}` exposes duplicate tool name `{name}`"
3385 );
3386 }
3387
3388 let mut def_seen = std::collections::HashSet::new();
3391 for def in cap.tool_definitions() {
3392 let name = def.name().to_string();
3393 assert!(
3394 !name.is_empty(),
3395 "capability `{id}` advertises a tool definition with an empty name"
3396 );
3397 assert!(
3398 def_seen.insert(name.clone()),
3399 "capability `{id}` advertises duplicate tool definition name `{name}`"
3400 );
3401 }
3402 }
3403 }
3404
3405 #[test]
3406 fn test_capability_registry_blueprint_with_capability() {
3407 struct BlueprintProviderCapability;
3408
3409 impl Capability for BlueprintProviderCapability {
3410 fn id(&self) -> &str {
3411 "blueprint_provider"
3412 }
3413 fn name(&self) -> &str {
3414 "Blueprint Provider"
3415 }
3416 fn description(&self) -> &str {
3417 "Capability that provides a blueprint for tests"
3418 }
3419 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3420 vec![AgentBlueprint {
3421 id: "test_blueprint",
3422 name: "Test Blueprint",
3423 description: "Blueprint for capability registry tests",
3424 model: BlueprintModel::Inherit,
3425 system_prompt: "Test prompt",
3426 tools: vec![],
3427 max_turns: None,
3428 config_schema: None,
3429 }]
3430 }
3431 }
3432
3433 let mut registry = CapabilityRegistry::new();
3434 registry.register(BlueprintProviderCapability);
3435
3436 let (capability_id, blueprint) = registry
3437 .blueprint_with_capability("test_blueprint")
3438 .expect("blueprint should resolve with capability id");
3439 assert_eq!(capability_id, "blueprint_provider");
3440 assert_eq!(blueprint.id, "test_blueprint");
3441 }
3442
3443 #[test]
3444 fn test_capability_registry_builder() {
3445 let registry = CapabilityRegistry::builder()
3446 .capability(NoopCapability)
3447 .capability(CurrentTimeCapability)
3448 .build();
3449
3450 assert!(registry.has("noop"));
3451 assert!(registry.has("current_time"));
3452 assert_eq!(registry.len(), 2);
3453 }
3454
3455 #[test]
3456 fn test_capability_status() {
3457 let registry = CapabilityRegistry::with_builtins();
3458
3459 let current_time = registry.get("current_time").unwrap();
3460 assert_eq!(current_time.status(), CapabilityStatus::Available);
3461
3462 let research = registry.get("research").unwrap();
3463 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3464 }
3465
3466 #[test]
3467 fn test_capability_icons_and_categories() {
3468 let registry = CapabilityRegistry::with_builtins();
3469
3470 let noop = registry.get("noop").unwrap();
3471 assert_eq!(noop.icon(), Some("circle-off"));
3472 assert_eq!(noop.category(), Some("Testing"));
3473
3474 let current_time = registry.get("current_time").unwrap();
3475 assert_eq!(current_time.icon(), Some("clock"));
3476 assert_eq!(current_time.category(), Some("Core"));
3477 }
3478
3479 #[test]
3480 fn test_system_prompt_preview_default_delegates_to_addition() {
3481 let registry = CapabilityRegistry::with_builtins();
3482
3483 let test_math = registry.get("test_math").unwrap();
3485 assert_eq!(
3486 test_math.system_prompt_preview().as_deref(),
3487 test_math.system_prompt_addition()
3488 );
3489
3490 let current_time = registry.get("current_time").unwrap();
3492 assert!(current_time.system_prompt_preview().is_none());
3493 assert!(current_time.system_prompt_addition().is_none());
3494 }
3495
3496 #[test]
3497 fn test_system_prompt_preview_dynamic_capability() {
3498 let registry = CapabilityRegistry::with_builtins();
3499 let cap = registry.get("agent_instructions").unwrap();
3500
3501 assert!(cap.system_prompt_addition().is_none());
3503 assert!(cap.system_prompt_preview().is_some());
3504 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3505 }
3506
3507 #[tokio::test]
3512 async fn test_apply_capabilities_empty() {
3513 let registry = CapabilityRegistry::with_builtins();
3514 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3515
3516 let applied =
3517 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3518
3519 assert_eq!(
3520 applied.runtime_agent.system_prompt,
3521 base_runtime_agent.system_prompt
3522 );
3523 assert!(applied.tool_registry.is_empty());
3524 assert!(applied.applied_ids.is_empty());
3525 }
3526
3527 #[tokio::test]
3528 async fn test_apply_capabilities_noop() {
3529 let registry = CapabilityRegistry::with_builtins();
3530 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3531
3532 let applied = apply_capabilities(
3533 base_runtime_agent.clone(),
3534 &["noop".to_string()],
3535 ®istry,
3536 &test_ctx(),
3537 )
3538 .await;
3539
3540 assert_eq!(
3542 applied.runtime_agent.system_prompt,
3543 base_runtime_agent.system_prompt
3544 );
3545 assert!(applied.tool_registry.is_empty());
3546 assert_eq!(applied.applied_ids, vec!["noop"]);
3547 }
3548
3549 #[tokio::test]
3550 async fn test_apply_capabilities_current_time() {
3551 let registry = CapabilityRegistry::with_builtins();
3552 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3553
3554 let applied = apply_capabilities(
3555 base_runtime_agent.clone(),
3556 &["current_time".to_string()],
3557 ®istry,
3558 &test_ctx(),
3559 )
3560 .await;
3561
3562 assert!(
3566 applied
3567 .runtime_agent
3568 .system_prompt
3569 .contains(FACTS_DYNAMIC_NOTE),
3570 "current_time should contribute the dynamic-facts note"
3571 );
3572 assert!(
3573 applied
3574 .runtime_agent
3575 .system_prompt
3576 .contains(&base_runtime_agent.system_prompt),
3577 "base prompt is preserved"
3578 );
3579 assert!(applied.tool_registry.has("get_current_time"));
3580 assert_eq!(applied.tool_registry.len(), 1);
3581 assert_eq!(applied.applied_ids, vec!["current_time"]);
3582 }
3583
3584 #[tokio::test]
3585 async fn test_apply_capabilities_skips_coming_soon() {
3586 let registry = CapabilityRegistry::with_builtins();
3587 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3588
3589 let applied = apply_capabilities(
3591 base_runtime_agent.clone(),
3592 &["research".to_string()],
3593 ®istry,
3594 &test_ctx(),
3595 )
3596 .await;
3597
3598 assert_eq!(
3600 applied.runtime_agent.system_prompt,
3601 base_runtime_agent.system_prompt
3602 );
3603 assert!(applied.applied_ids.is_empty()); }
3605
3606 #[tokio::test]
3607 async fn test_apply_capabilities_multiple() {
3608 let registry = CapabilityRegistry::with_builtins();
3609 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3610
3611 let applied = apply_capabilities(
3612 base_runtime_agent.clone(),
3613 &["noop".to_string(), "current_time".to_string()],
3614 ®istry,
3615 &test_ctx(),
3616 )
3617 .await;
3618
3619 assert!(applied.tool_registry.has("get_current_time"));
3620 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3621 }
3622
3623 #[tokio::test]
3624 async fn test_apply_capabilities_preserves_order() {
3625 let registry = CapabilityRegistry::with_builtins();
3626 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3627
3628 let applied = apply_capabilities(
3630 base_runtime_agent,
3631 &["current_time".to_string(), "noop".to_string()],
3632 ®istry,
3633 &test_ctx(),
3634 )
3635 .await;
3636
3637 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3638 }
3639
3640 #[tokio::test]
3641 async fn test_apply_capabilities_test_math() {
3642 let registry = CapabilityRegistry::with_builtins();
3643 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3644
3645 let applied = apply_capabilities(
3646 base_runtime_agent.clone(),
3647 &["test_math".to_string()],
3648 ®istry,
3649 &test_ctx(),
3650 )
3651 .await;
3652
3653 assert!(
3655 !applied
3656 .runtime_agent
3657 .system_prompt
3658 .contains("<capability id=\"test_math\">")
3659 );
3660 assert!(
3662 applied
3663 .runtime_agent
3664 .system_prompt
3665 .contains("You are a helpful assistant.")
3666 );
3667 assert!(applied.tool_registry.has("add"));
3668 assert!(applied.tool_registry.has("subtract"));
3669 assert!(applied.tool_registry.has("multiply"));
3670 assert!(applied.tool_registry.has("divide"));
3671 assert_eq!(applied.tool_registry.len(), 4);
3672 }
3673
3674 #[tokio::test]
3675 async fn test_apply_capabilities_test_weather() {
3676 let registry = CapabilityRegistry::with_builtins();
3677 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3678
3679 let applied = apply_capabilities(
3680 base_runtime_agent.clone(),
3681 &["test_weather".to_string()],
3682 ®istry,
3683 &test_ctx(),
3684 )
3685 .await;
3686
3687 assert!(
3689 !applied
3690 .runtime_agent
3691 .system_prompt
3692 .contains("<capability id=\"test_weather\">")
3693 );
3694 assert!(applied.tool_registry.has("get_weather"));
3695 assert!(applied.tool_registry.has("get_forecast"));
3696 assert_eq!(applied.tool_registry.len(), 2);
3697 }
3698
3699 #[tokio::test]
3700 async fn test_apply_capabilities_test_math_and_test_weather() {
3701 let registry = CapabilityRegistry::with_builtins();
3702 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3703
3704 let applied = apply_capabilities(
3705 base_runtime_agent.clone(),
3706 &["test_math".to_string(), "test_weather".to_string()],
3707 ®istry,
3708 &test_ctx(),
3709 )
3710 .await;
3711
3712 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3715 assert!(applied.tool_registry.has("get_weather"));
3716 }
3717
3718 #[tokio::test]
3719 async fn test_apply_capabilities_stateless_todo_list() {
3720 let registry = CapabilityRegistry::with_builtins();
3721 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3722
3723 let applied = apply_capabilities(
3724 base_runtime_agent.clone(),
3725 &["stateless_todo_list".to_string()],
3726 ®istry,
3727 &test_ctx(),
3728 )
3729 .await;
3730
3731 assert!(
3733 applied
3734 .runtime_agent
3735 .system_prompt
3736 .contains("Task Management")
3737 );
3738 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3739 assert!(applied.tool_registry.has("write_todos"));
3740 assert_eq!(applied.tool_registry.len(), 1);
3741 }
3742
3743 #[tokio::test]
3744 async fn test_apply_capabilities_web_fetch() {
3745 let registry = CapabilityRegistry::with_builtins();
3746 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3747
3748 let applied = apply_capabilities(
3749 base_runtime_agent.clone(),
3750 &["web_fetch".to_string()],
3751 ®istry,
3752 &test_ctx(),
3753 )
3754 .await;
3755
3756 assert!(
3758 applied
3759 .runtime_agent
3760 .system_prompt
3761 .contains(&base_runtime_agent.system_prompt)
3762 );
3763 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3764 assert!(applied.tool_registry.has("web_fetch"));
3765 assert_eq!(applied.tool_registry.len(), 1);
3766 }
3767
3768 #[tokio::test]
3773 async fn test_xml_tags_wrap_capability_prompts() {
3774 let registry = CapabilityRegistry::with_builtins();
3775 let collected =
3776 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3777 .await;
3778
3779 assert_eq!(collected.system_prompt_parts.len(), 1);
3780 let part = &collected.system_prompt_parts[0];
3781 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3782 assert!(part.ends_with("</capability>"));
3783 assert!(part.contains("Task Management"));
3784 }
3785
3786 #[tokio::test]
3787 async fn test_xml_tags_multiple_capabilities() {
3788 let registry = CapabilityRegistry::with_builtins();
3789 let collected = collect_capabilities(
3790 &[
3791 "stateless_todo_list".to_string(),
3792 "session_schedule".to_string(),
3793 ],
3794 ®istry,
3795 &test_ctx(),
3796 )
3797 .await;
3798
3799 assert_eq!(collected.system_prompt_parts.len(), 2);
3800 assert!(
3801 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3802 );
3803 assert!(
3804 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3805 );
3806
3807 let prefix = collected.system_prompt_prefix().unwrap();
3808 assert!(prefix.contains("</capability>\n\n<capability"));
3810 }
3811
3812 #[tokio::test]
3813 async fn test_xml_tags_system_prompt_wrapping() {
3814 let registry = CapabilityRegistry::with_builtins();
3815 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3816
3817 let applied = apply_capabilities(
3818 base,
3819 &["stateless_todo_list".to_string()],
3820 ®istry,
3821 &test_ctx(),
3822 )
3823 .await;
3824
3825 let prompt = &applied.runtime_agent.system_prompt;
3826 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3827 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3829 assert!(prompt.contains("</capability>"));
3830 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3832 }
3833
3834 #[tokio::test]
3835 async fn test_no_xml_wrapping_without_capabilities() {
3836 let registry = CapabilityRegistry::with_builtins();
3837 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3838
3839 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3840
3841 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3843 assert!(
3844 !applied
3845 .runtime_agent
3846 .system_prompt
3847 .contains("<system-prompt>")
3848 );
3849 }
3850
3851 #[tokio::test]
3852 async fn test_no_xml_wrapping_for_noop_capability() {
3853 let registry = CapabilityRegistry::with_builtins();
3854 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3855
3856 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3858
3859 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3860 assert!(
3861 !applied
3862 .runtime_agent
3863 .system_prompt
3864 .contains("<system-prompt>")
3865 );
3866 }
3867
3868 #[tokio::test]
3873 async fn test_collect_capabilities_includes_mounts() {
3874 let registry = CapabilityRegistry::with_builtins();
3875
3876 let collected =
3877 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3878
3879 assert!(!collected.mounts.is_empty());
3880 assert_eq!(collected.mounts.len(), 1);
3881 assert_eq!(collected.mounts[0].path, "/samples");
3882 assert!(collected.mounts[0].is_readonly());
3883 }
3884
3885 #[tokio::test]
3886 async fn test_collect_capabilities_empty_mounts_by_default() {
3887 let registry = CapabilityRegistry::with_builtins();
3888
3889 let collected =
3891 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3892
3893 assert!(collected.mounts.is_empty());
3894 }
3895
3896 #[tokio::test]
3897 async fn test_dynamic_facts_add_note_without_static_block() {
3898 let registry = CapabilityRegistry::with_builtins();
3902 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3903 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3904 let prompt = collected.system_prompt_parts.join("\n");
3905 assert!(
3906 prompt.contains(FACTS_DYNAMIC_NOTE),
3907 "dynamic-facts note should be in the cached prompt"
3908 );
3909 assert!(
3910 !prompt.contains("<facts>\n"),
3911 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
3912 );
3913 }
3914
3915 #[tokio::test]
3916 async fn test_static_facts_fold_into_prompt() {
3917 struct StaticFactCap;
3918 impl Capability for StaticFactCap {
3919 fn id(&self) -> &str {
3920 "test_static_fact"
3921 }
3922 fn name(&self) -> &str {
3923 "Static Fact"
3924 }
3925 fn description(&self) -> &str {
3926 "test"
3927 }
3928 fn status(&self) -> CapabilityStatus {
3929 CapabilityStatus::Available
3930 }
3931 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
3932 vec![Fact::stat("workspace_root", "/workspace")]
3933 }
3934 }
3935 let mut registry = CapabilityRegistry::new();
3936 registry.register(StaticFactCap);
3937 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
3938 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3939 let prompt = collected.system_prompt_parts.join("\n");
3940 assert!(
3941 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
3942 "static fact should fold into the cached prompt; got: {prompt}"
3943 );
3944 assert!(
3945 !prompt.contains(FACTS_DYNAMIC_NOTE),
3946 "no dynamic note when only static facts exist"
3947 );
3948 }
3949
3950 #[test]
3951 fn test_collect_dynamic_facts_returns_current_time() {
3952 let registry = CapabilityRegistry::with_builtins();
3953 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3954 let facts = collect_dynamic_facts(
3955 &configs,
3956 ®istry,
3957 None,
3958 &FactsContext::new(SessionId::new()),
3959 );
3960 assert_eq!(facts.len(), 1);
3961 assert_eq!(facts[0].key, "current_time");
3962 assert_eq!(facts[0].volatility, Volatility::Dynamic);
3963 }
3964
3965 #[tokio::test]
3966 async fn test_collect_capabilities_combines_mounts() {
3967 let registry = CapabilityRegistry::with_builtins();
3968
3969 let collected = collect_capabilities(
3972 &["sample_data".to_string(), "current_time".to_string()],
3973 ®istry,
3974 &test_ctx(),
3975 )
3976 .await;
3977
3978 assert_eq!(collected.mounts.len(), 1);
3979 assert!(
3981 collected
3982 .applied_ids
3983 .iter()
3984 .any(|id| id == "session_file_system")
3985 );
3986 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
3987 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
3988 }
3989
3990 #[test]
3991 fn test_sample_data_capability() {
3992 let registry = CapabilityRegistry::with_builtins();
3993 let cap = registry.get("sample_data").unwrap();
3994
3995 assert_eq!(cap.id(), "sample_data");
3996 assert_eq!(cap.name(), "Sample Data");
3997 assert_eq!(cap.status(), CapabilityStatus::Available);
3998
3999 assert!(cap.system_prompt_addition().is_some());
4001 assert!(cap.tools().is_empty());
4002
4003 assert!(!cap.mounts().is_empty());
4005 }
4006
4007 #[test]
4012 fn test_resolve_dependencies_empty() {
4013 let registry = CapabilityRegistry::with_builtins();
4014
4015 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4016
4017 assert!(resolved.resolved_ids.is_empty());
4018 assert!(resolved.added_as_dependencies.is_empty());
4019 assert!(resolved.user_selected.is_empty());
4020 }
4021
4022 #[test]
4023 fn test_resolve_dependencies_no_deps() {
4024 let registry = CapabilityRegistry::with_builtins();
4025
4026 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4028
4029 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4030 assert!(resolved.added_as_dependencies.is_empty());
4031 }
4032
4033 #[test]
4034 fn test_resolve_dependencies_with_deps() {
4035 let registry = CapabilityRegistry::with_builtins();
4036
4037 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4039
4040 assert_eq!(resolved.resolved_ids.len(), 2);
4042 let fs_pos = resolved
4043 .resolved_ids
4044 .iter()
4045 .position(|id| id == "session_file_system")
4046 .unwrap();
4047 let sd_pos = resolved
4048 .resolved_ids
4049 .iter()
4050 .position(|id| id == "sample_data")
4051 .unwrap();
4052 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4053
4054 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4056 }
4057
4058 #[test]
4059 fn test_resolve_dependencies_already_selected() {
4060 let registry = CapabilityRegistry::with_builtins();
4061
4062 let resolved = resolve_dependencies(
4064 &["session_file_system".to_string(), "sample_data".to_string()],
4065 ®istry,
4066 )
4067 .unwrap();
4068
4069 assert_eq!(resolved.resolved_ids.len(), 2);
4070 assert!(resolved.added_as_dependencies.is_empty());
4072 }
4073
4074 #[test]
4075 fn test_resolve_dependencies_preserves_order() {
4076 let registry = CapabilityRegistry::with_builtins();
4077
4078 let resolved =
4080 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4081 .unwrap();
4082
4083 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4084 }
4085
4086 #[test]
4087 fn test_resolve_dependencies_unknown_capability() {
4088 let registry = CapabilityRegistry::with_builtins();
4089
4090 let resolved =
4092 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4093
4094 assert!(resolved.resolved_ids.is_empty());
4095 }
4096
4097 #[test]
4098 fn test_get_dependencies() {
4099 let registry = CapabilityRegistry::with_builtins();
4100
4101 let deps = get_dependencies("sample_data", ®istry);
4103 assert_eq!(deps, vec!["session_file_system"]);
4104
4105 let deps = get_dependencies("current_time", ®istry);
4107 assert!(deps.is_empty());
4108
4109 let deps = get_dependencies("unknown", ®istry);
4111 assert!(deps.is_empty());
4112 }
4113
4114 #[test]
4115 fn test_sample_data_has_dependency() {
4116 let registry = CapabilityRegistry::with_builtins();
4117 let cap = registry.get("sample_data").unwrap();
4118
4119 let deps = cap.dependencies();
4120 assert_eq!(deps.len(), 1);
4121 assert_eq!(deps[0], "session_file_system");
4122 }
4123
4124 #[test]
4125 fn test_noop_has_no_dependencies() {
4126 let registry = CapabilityRegistry::with_builtins();
4127 let cap = registry.get("noop").unwrap();
4128
4129 assert!(cap.dependencies().is_empty());
4130 }
4131
4132 #[test]
4136 fn test_circular_dependency_error() {
4137 struct CapA;
4139 struct CapB;
4140
4141 impl Capability for CapA {
4142 fn id(&self) -> &str {
4143 "test_cap_a"
4144 }
4145 fn name(&self) -> &str {
4146 "Test A"
4147 }
4148 fn description(&self) -> &str {
4149 "Test capability A"
4150 }
4151 fn dependencies(&self) -> Vec<&'static str> {
4152 vec!["test_cap_b"]
4153 }
4154 }
4155
4156 impl Capability for CapB {
4157 fn id(&self) -> &str {
4158 "test_cap_b"
4159 }
4160 fn name(&self) -> &str {
4161 "Test B"
4162 }
4163 fn description(&self) -> &str {
4164 "Test capability B"
4165 }
4166 fn dependencies(&self) -> Vec<&'static str> {
4167 vec!["test_cap_a"]
4168 }
4169 }
4170
4171 let mut registry = CapabilityRegistry::new();
4172 registry.register(CapA);
4173 registry.register(CapB);
4174
4175 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4176
4177 assert!(result.is_err());
4178 match result.unwrap_err() {
4179 DependencyError::CircularDependency { capability_id, .. } => {
4180 assert_eq!(capability_id, "test_cap_a");
4181 }
4182 _ => panic!("Expected CircularDependency error"),
4183 }
4184 }
4185
4186 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4191
4192 struct FilterTestCapability {
4194 priority: i32,
4195 }
4196
4197 impl Capability for FilterTestCapability {
4198 fn id(&self) -> &str {
4199 "filter_test"
4200 }
4201 fn name(&self) -> &str {
4202 "Filter Test"
4203 }
4204 fn description(&self) -> &str {
4205 "Test capability with message filter"
4206 }
4207 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4208 Some(Arc::new(FilterTestProvider {
4209 priority: self.priority,
4210 }))
4211 }
4212 }
4213
4214 struct FilterTestProvider {
4215 priority: i32,
4216 }
4217
4218 impl MessageFilterProvider for FilterTestProvider {
4219 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4220 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4222 query
4223 .filters
4224 .push(MessageFilter::Search(search.to_string()));
4225 }
4226 }
4227
4228 fn priority(&self) -> i32 {
4229 self.priority
4230 }
4231 }
4232
4233 #[tokio::test]
4234 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4235 let registry = CapabilityRegistry::with_builtins();
4236 let configs = vec![AgentCapabilityConfig {
4237 capability_ref: CapabilityId::new("current_time"),
4238 config: serde_json::json!({}),
4239 }];
4240
4241 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4242
4243 assert!(collected.message_filter_providers.is_empty());
4244 assert!(!collected.has_message_filters());
4245 }
4246
4247 #[tokio::test]
4248 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4249 let mut registry = CapabilityRegistry::new();
4250 registry.register(FilterTestCapability { priority: 0 });
4251
4252 let configs = vec![AgentCapabilityConfig {
4253 capability_ref: CapabilityId::new("filter_test"),
4254 config: serde_json::json!({ "search": "hello" }),
4255 }];
4256
4257 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4258
4259 assert_eq!(collected.message_filter_providers.len(), 1);
4260 assert!(collected.has_message_filters());
4261 }
4262
4263 #[tokio::test]
4264 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4265 struct HighPriorityCapability;
4267 struct LowPriorityCapability;
4268
4269 impl Capability for HighPriorityCapability {
4270 fn id(&self) -> &str {
4271 "high_priority"
4272 }
4273 fn name(&self) -> &str {
4274 "High Priority"
4275 }
4276 fn description(&self) -> &str {
4277 "Test"
4278 }
4279 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4280 Some(Arc::new(FilterTestProvider { priority: 10 }))
4281 }
4282 }
4283
4284 impl Capability for LowPriorityCapability {
4285 fn id(&self) -> &str {
4286 "low_priority"
4287 }
4288 fn name(&self) -> &str {
4289 "Low Priority"
4290 }
4291 fn description(&self) -> &str {
4292 "Test"
4293 }
4294 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4295 Some(Arc::new(FilterTestProvider { priority: -5 }))
4296 }
4297 }
4298
4299 let mut registry = CapabilityRegistry::new();
4300 registry.register(HighPriorityCapability);
4301 registry.register(LowPriorityCapability);
4302
4303 let configs = vec![
4305 AgentCapabilityConfig {
4306 capability_ref: CapabilityId::new("high_priority"),
4307 config: serde_json::json!({}),
4308 },
4309 AgentCapabilityConfig {
4310 capability_ref: CapabilityId::new("low_priority"),
4311 config: serde_json::json!({}),
4312 },
4313 ];
4314
4315 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4316
4317 assert_eq!(collected.message_filter_providers.len(), 2);
4319 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4320 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4321 }
4322
4323 #[tokio::test]
4324 async fn test_collected_capabilities_apply_message_filters() {
4325 let mut registry = CapabilityRegistry::new();
4326 registry.register(FilterTestCapability { priority: 0 });
4327
4328 let configs = vec![AgentCapabilityConfig {
4329 capability_ref: CapabilityId::new("filter_test"),
4330 config: serde_json::json!({ "search": "test_query" }),
4331 }];
4332
4333 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4334
4335 let session_id: SessionId = Uuid::now_v7().into();
4337 let mut query = MessageQuery::new(session_id);
4338
4339 collected.apply_message_filters(&mut query);
4340
4341 assert_eq!(query.filters.len(), 1);
4343 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4344 }
4345
4346 #[tokio::test]
4347 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4348 struct SearchCapability {
4349 id: &'static str,
4350 search_term: &'static str,
4351 priority: i32,
4352 }
4353
4354 struct SearchProvider {
4355 search_term: &'static str,
4356 priority: i32,
4357 }
4358
4359 impl MessageFilterProvider for SearchProvider {
4360 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4361 query
4362 .filters
4363 .push(MessageFilter::Search(self.search_term.to_string()));
4364 }
4365
4366 fn priority(&self) -> i32 {
4367 self.priority
4368 }
4369 }
4370
4371 impl Capability for SearchCapability {
4372 fn id(&self) -> &str {
4373 self.id
4374 }
4375 fn name(&self) -> &str {
4376 "Search"
4377 }
4378 fn description(&self) -> &str {
4379 "Test"
4380 }
4381 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4382 Some(Arc::new(SearchProvider {
4383 search_term: self.search_term,
4384 priority: self.priority,
4385 }))
4386 }
4387 }
4388
4389 let mut registry = CapabilityRegistry::new();
4390 registry.register(SearchCapability {
4391 id: "cap_a",
4392 search_term: "alpha",
4393 priority: 5,
4394 });
4395 registry.register(SearchCapability {
4396 id: "cap_b",
4397 search_term: "beta",
4398 priority: 1,
4399 });
4400 registry.register(SearchCapability {
4401 id: "cap_c",
4402 search_term: "gamma",
4403 priority: 10,
4404 });
4405
4406 let configs = vec![
4407 AgentCapabilityConfig {
4408 capability_ref: CapabilityId::new("cap_a"),
4409 config: serde_json::json!({}),
4410 },
4411 AgentCapabilityConfig {
4412 capability_ref: CapabilityId::new("cap_b"),
4413 config: serde_json::json!({}),
4414 },
4415 AgentCapabilityConfig {
4416 capability_ref: CapabilityId::new("cap_c"),
4417 config: serde_json::json!({}),
4418 },
4419 ];
4420
4421 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4422
4423 let session_id: SessionId = Uuid::now_v7().into();
4424 let mut query = MessageQuery::new(session_id);
4425
4426 collected.apply_message_filters(&mut query);
4427
4428 assert_eq!(query.filters.len(), 3);
4430 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4431 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4432 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4433 }
4434
4435 #[test]
4436 fn test_capability_without_message_filter_returns_none() {
4437 let registry = CapabilityRegistry::with_builtins();
4438
4439 let noop = registry.get("noop").unwrap();
4440 assert!(noop.message_filter_provider().is_none());
4441
4442 let current_time = registry.get("current_time").unwrap();
4443 assert!(current_time.message_filter_provider().is_none());
4444 }
4445
4446 #[tokio::test]
4447 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4448 let mut registry = CapabilityRegistry::new();
4449 registry.register(FilterTestCapability { priority: 0 });
4450
4451 let test_config = serde_json::json!({
4452 "search": "custom_search",
4453 "extra_field": 42
4454 });
4455
4456 let configs = vec![AgentCapabilityConfig {
4457 capability_ref: CapabilityId::new("filter_test"),
4458 config: test_config.clone(),
4459 }];
4460
4461 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4462
4463 assert_eq!(collected.message_filter_providers.len(), 1);
4465 let (_, stored_config) = &collected.message_filter_providers[0];
4466 assert_eq!(*stored_config, test_config);
4467 }
4468
4469 #[test]
4474 fn test_collect_message_filters_only_collects_filters() {
4475 let mut registry = CapabilityRegistry::new();
4476 registry.register(FilterTestCapability { priority: 0 });
4477
4478 let configs = vec![AgentCapabilityConfig {
4479 capability_ref: CapabilityId::new("filter_test"),
4480 config: serde_json::json!({ "search": "test_query" }),
4481 }];
4482
4483 let collected = collect_message_filters_only(&configs, ®istry);
4484
4485 let session_id: SessionId = Uuid::now_v7().into();
4486 let mut query = MessageQuery::new(session_id);
4487 collected.apply_message_filters(&mut query);
4488
4489 assert_eq!(query.filters.len(), 1);
4490 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4491 }
4492
4493 #[test]
4494 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4495 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4496
4497 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4499 assert_eq!(with["compaction_active"], serde_json::json!(true));
4500 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4501
4502 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4503 assert!(without.get("compaction_active").is_none());
4504
4505 let other = message_filter_config_for("other", &base, true);
4507 assert!(other.get("compaction_active").is_none());
4508
4509 let null_base = message_filter_config_for(
4511 INFINITY_CONTEXT_CAPABILITY_ID,
4512 &serde_json::Value::Null,
4513 true,
4514 );
4515 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4516 }
4517
4518 #[test]
4519 fn test_infinity_context_defers_to_compaction_end_to_end() {
4520 use crate::message::Message;
4521
4522 let mut registry = CapabilityRegistry::new();
4523 registry.register(InfinityContextCapability);
4524 registry.register(CompactionCapability);
4525
4526 let tight = serde_json::json!({
4527 "context_budget_tokens": 1,
4528 "min_recent_messages": 1
4529 });
4530
4531 let solo = vec![AgentCapabilityConfig {
4533 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4534 config: tight.clone(),
4535 }];
4536 let mut messages = vec![
4537 Message::user("task"),
4538 Message::assistant("old ".repeat(400)),
4539 Message::user("recent"),
4540 ];
4541 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4542 assert!(
4543 messages
4544 .iter()
4545 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4546 "infinity context alone should trim and notice"
4547 );
4548
4549 let both = vec![
4551 AgentCapabilityConfig {
4552 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4553 config: tight,
4554 },
4555 AgentCapabilityConfig {
4556 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4557 config: serde_json::json!({}),
4558 },
4559 ];
4560 let mut messages = vec![
4561 Message::user("task"),
4562 Message::assistant("old ".repeat(400)),
4563 Message::user("recent"),
4564 ];
4565 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4566 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4567 assert!(
4568 messages
4569 .iter()
4570 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4571 "no hidden-history notice when compaction is the active reducer"
4572 );
4573 }
4574
4575 #[test]
4576 fn test_compaction_is_enabled_detects_compaction() {
4577 let mut registry = CapabilityRegistry::new();
4578 registry.register(CompactionCapability);
4579
4580 let with_compaction = vec![AgentCapabilityConfig {
4581 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4582 config: serde_json::json!({}),
4583 }];
4584 assert!(compaction_is_enabled(&with_compaction, ®istry));
4585
4586 let without = vec![AgentCapabilityConfig {
4587 capability_ref: CapabilityId::new("current_time"),
4588 config: serde_json::json!({}),
4589 }];
4590 assert!(!compaction_is_enabled(&without, ®istry));
4591 }
4592
4593 #[test]
4594 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4595 let registry = CapabilityRegistry::new();
4596
4597 let configs = vec![AgentCapabilityConfig {
4598 capability_ref: CapabilityId::new("nonexistent"),
4599 config: serde_json::json!({}),
4600 }];
4601
4602 let collected = collect_message_filters_only(&configs, ®istry);
4603 assert!(collected.message_filter_providers.is_empty());
4604 }
4605
4606 #[test]
4607 fn test_collect_message_filters_only_preserves_priority_order() {
4608 struct PriorityFilterCap {
4609 id: &'static str,
4610 search_term: &'static str,
4611 priority: i32,
4612 }
4613
4614 struct PriorityFilterProvider {
4615 search_term: &'static str,
4616 priority: i32,
4617 }
4618
4619 impl Capability for PriorityFilterCap {
4620 fn id(&self) -> &str {
4621 self.id
4622 }
4623 fn name(&self) -> &str {
4624 self.id
4625 }
4626 fn description(&self) -> &str {
4627 "priority test"
4628 }
4629 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4630 Some(Arc::new(PriorityFilterProvider {
4631 search_term: self.search_term,
4632 priority: self.priority,
4633 }))
4634 }
4635 }
4636
4637 impl MessageFilterProvider for PriorityFilterProvider {
4638 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4639 query
4640 .filters
4641 .push(MessageFilter::Search(self.search_term.to_string()));
4642 }
4643 fn priority(&self) -> i32 {
4644 self.priority
4645 }
4646 }
4647
4648 let mut registry = CapabilityRegistry::new();
4649 registry.register(PriorityFilterCap {
4650 id: "gamma",
4651 search_term: "gamma",
4652 priority: 10,
4653 });
4654 registry.register(PriorityFilterCap {
4655 id: "alpha",
4656 search_term: "alpha",
4657 priority: 5,
4658 });
4659 registry.register(PriorityFilterCap {
4660 id: "beta",
4661 search_term: "beta",
4662 priority: 1,
4663 });
4664
4665 let configs = vec![
4666 AgentCapabilityConfig {
4667 capability_ref: CapabilityId::new("gamma"),
4668 config: serde_json::json!({}),
4669 },
4670 AgentCapabilityConfig {
4671 capability_ref: CapabilityId::new("alpha"),
4672 config: serde_json::json!({}),
4673 },
4674 AgentCapabilityConfig {
4675 capability_ref: CapabilityId::new("beta"),
4676 config: serde_json::json!({}),
4677 },
4678 ];
4679
4680 let collected = collect_message_filters_only(&configs, ®istry);
4681
4682 let session_id: SessionId = Uuid::now_v7().into();
4683 let mut query = MessageQuery::new(session_id);
4684 collected.apply_message_filters(&mut query);
4685
4686 assert_eq!(query.filters.len(), 3);
4688 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4689 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4690 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4691 }
4692
4693 #[test]
4694 fn test_collect_message_filters_only_post_load_invoked() {
4695 use crate::message::Message;
4696
4697 struct PostLoadCap;
4698 struct PostLoadProvider;
4699
4700 impl Capability for PostLoadCap {
4701 fn id(&self) -> &str {
4702 "post_load_test"
4703 }
4704 fn name(&self) -> &str {
4705 "PostLoad Test"
4706 }
4707 fn description(&self) -> &str {
4708 "test"
4709 }
4710 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4711 Some(Arc::new(PostLoadProvider))
4712 }
4713 }
4714
4715 impl MessageFilterProvider for PostLoadProvider {
4716 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4717 fn priority(&self) -> i32 {
4718 0
4719 }
4720 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4721 messages.reverse();
4723 }
4724 }
4725
4726 let mut registry = CapabilityRegistry::new();
4727 registry.register(PostLoadCap);
4728
4729 let configs = vec![AgentCapabilityConfig {
4730 capability_ref: CapabilityId::new("post_load_test"),
4731 config: serde_json::json!({}),
4732 }];
4733
4734 let collected = collect_message_filters_only(&configs, ®istry);
4735
4736 let mut messages = vec![Message::user("first"), Message::user("second")];
4737 collected.apply_post_load_filters(&mut messages);
4738
4739 assert_eq!(messages[0].text(), Some("second"));
4741 assert_eq!(messages[1].text(), Some("first"));
4742 }
4743
4744 #[test]
4745 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4746 use crate::tool_types::ToolCall;
4747
4748 fn tool_heavy_messages() -> Vec<Message> {
4749 let mut messages = vec![Message::user("inspect files repeatedly")];
4750 for index in 0..9 {
4751 let call_id = format!("call_{index}");
4752 messages.push(Message::assistant_with_tools(
4753 "",
4754 vec![ToolCall {
4755 id: call_id.clone(),
4756 name: "read_file".to_string(),
4757 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4758 }],
4759 ));
4760 messages.push(Message::tool_result(
4761 call_id,
4762 Some(serde_json::json!({
4763 "path": "/workspace/src/lib.rs",
4764 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4765 "total_lines": 1000,
4766 "lines_shown": {"start": 1, "end": 1000},
4767 "truncated": false
4768 })),
4769 None,
4770 ));
4771 }
4772 messages
4773 }
4774
4775 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4776 messages[2]
4777 .tool_result_content()
4778 .and_then(|result| result.result.as_ref())
4779 .and_then(|result| result.get("masked"))
4780 .and_then(|masked| masked.as_bool())
4781 .unwrap_or(false)
4782 }
4783
4784 let mut registry = CapabilityRegistry::new();
4785 registry.register(CompactionCapability);
4786 let context = ModelViewContext {
4787 session_id: SessionId::new(),
4788 prior_usage: None,
4789 };
4790
4791 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4792 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4793 assert!(!first_tool_result_is_masked(&unmasked));
4794
4795 let compaction = collect_model_view_providers(
4796 &[AgentCapabilityConfig {
4797 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4798 config: serde_json::json!({}),
4799 }],
4800 ®istry,
4801 None,
4802 );
4803 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4804 assert!(first_tool_result_is_masked(&masked));
4805 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4806 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4807 }
4808
4809 struct DelegatingFilterCap {
4812 id: &'static str,
4813 inner: std::sync::Arc<InnerFilterCap>,
4814 }
4815 struct InnerFilterCap;
4816
4817 impl Capability for InnerFilterCap {
4818 fn id(&self) -> &str {
4819 "inner_filter"
4820 }
4821 fn name(&self) -> &str {
4822 "Inner Filter"
4823 }
4824 fn description(&self) -> &str {
4825 "inner"
4826 }
4827 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4828 Some(std::sync::Arc::new(SentinelFilter))
4829 }
4830 }
4831 struct SentinelFilter;
4832 impl MessageFilterProvider for SentinelFilter {
4833 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4834 }
4835 impl Capability for DelegatingFilterCap {
4836 fn id(&self) -> &str {
4837 self.id
4838 }
4839 fn name(&self) -> &str {
4840 "Delegating Filter"
4841 }
4842 fn description(&self) -> &str {
4843 "delegating"
4844 }
4845 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4846 None }
4848 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4849 Some(&*self.inner)
4850 }
4851 }
4852
4853 #[test]
4854 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4855 let inner = std::sync::Arc::new(InnerFilterCap);
4856 let outer = DelegatingFilterCap {
4857 id: "delegating_filter",
4858 inner: inner.clone(),
4859 };
4860
4861 let mut registry = CapabilityRegistry::new();
4862 registry.register(outer);
4863
4864 let configs = vec![AgentCapabilityConfig {
4865 capability_ref: CapabilityId::new("delegating_filter"),
4866 config: serde_json::json!({}),
4867 }];
4868
4869 let collected = collect_message_filters_only(&configs, ®istry);
4872 assert_eq!(
4873 collected.message_filter_providers.len(),
4874 1,
4875 "provider from resolved inner capability must be collected"
4876 );
4877 }
4878
4879 struct DelegatingMvpCap {
4880 id: &'static str,
4881 inner: std::sync::Arc<InnerMvpCap>,
4882 }
4883 struct InnerMvpCap;
4884
4885 impl Capability for InnerMvpCap {
4886 fn id(&self) -> &str {
4887 "inner_mvp"
4888 }
4889 fn name(&self) -> &str {
4890 "Inner MVP"
4891 }
4892 fn description(&self) -> &str {
4893 "inner"
4894 }
4895 fn model_view_provider(
4896 &self,
4897 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4898 struct NoopMvp;
4900 impl crate::capabilities::ModelViewProvider for NoopMvp {
4901 fn apply_model_view(
4902 &self,
4903 messages: Vec<Message>,
4904 _config: &serde_json::Value,
4905 _context: &ModelViewContext<'_>,
4906 ) -> Vec<Message> {
4907 messages
4908 }
4909 }
4910 Some(std::sync::Arc::new(NoopMvp))
4911 }
4912 }
4913 impl Capability for DelegatingMvpCap {
4914 fn id(&self) -> &str {
4915 self.id
4916 }
4917 fn name(&self) -> &str {
4918 "Delegating MVP"
4919 }
4920 fn description(&self) -> &str {
4921 "delegating"
4922 }
4923 fn model_view_provider(
4924 &self,
4925 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4926 None }
4928 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4929 Some(&*self.inner)
4930 }
4931 }
4932
4933 #[test]
4934 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4935 let inner = std::sync::Arc::new(InnerMvpCap);
4936 let outer = DelegatingMvpCap {
4937 id: "delegating_mvp",
4938 inner: inner.clone(),
4939 };
4940
4941 let mut registry = CapabilityRegistry::new();
4942 registry.register(outer);
4943
4944 let configs = vec![AgentCapabilityConfig {
4945 capability_ref: CapabilityId::new("delegating_mvp"),
4946 config: serde_json::json!({}),
4947 }];
4948
4949 let collected = collect_model_view_providers(&configs, ®istry, None);
4952 assert_eq!(
4953 collected.model_view_providers.len(),
4954 1,
4955 "provider from resolved inner capability must be collected"
4956 );
4957 }
4958
4959 #[tokio::test]
4969 async fn test_bashkit_shell_capability_produces_bash_tool() {
4970 let registry = CapabilityRegistry::with_builtins();
4971 let collected =
4972 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
4973
4974 let tool_names: Vec<&str> = collected
4975 .tool_definitions
4976 .iter()
4977 .map(|t| t.name())
4978 .collect();
4979 assert!(
4980 tool_names.contains(&"bash"),
4981 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
4982 tool_names
4983 );
4984 assert!(
4985 !collected.tools.is_empty(),
4986 "bashkit_shell must provide tool implementations"
4987 );
4988 }
4989
4990 #[tokio::test]
4991 async fn test_generic_harness_capability_set_produces_bash_tool() {
4992 let generic_harness_caps = vec![
4995 "session_file_system".to_string(),
4996 "bashkit_shell".to_string(),
4997 "web_fetch".to_string(),
4998 "session_storage".to_string(),
4999 "session".to_string(),
5000 "agent_instructions".to_string(),
5001 "skills".to_string(),
5002 "infinity_context".to_string(),
5003 "auto_tool_search".to_string(),
5004 ];
5005
5006 let registry = CapabilityRegistry::with_builtins();
5007 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5008
5009 let tool_names: Vec<&str> = collected
5010 .tool_definitions
5011 .iter()
5012 .map(|t| t.name())
5013 .collect();
5014 assert!(
5015 tool_names.contains(&"bash"),
5016 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5017 tool_names
5018 );
5019 }
5020
5021 #[tokio::test]
5022 async fn test_collect_capabilities_tool_count_matches_definitions() {
5023 let registry = CapabilityRegistry::with_builtins();
5026 let collected =
5027 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5028
5029 assert_eq!(
5030 collected.tools.len(),
5031 collected.tool_definitions.len(),
5032 "tool implementations ({}) must match tool definitions ({})",
5033 collected.tools.len(),
5034 collected.tool_definitions.len(),
5035 );
5036 }
5037
5038 #[tokio::test]
5042 async fn test_collect_capabilities_resolves_dependencies() {
5043 let registry = CapabilityRegistry::with_builtins();
5046 let collected =
5047 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5048
5049 assert!(
5051 collected
5052 .applied_ids
5053 .iter()
5054 .any(|id| id == "session_file_system"),
5055 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5056 collected.applied_ids
5057 );
5058
5059 let tool_names: Vec<&str> = collected
5060 .tool_definitions
5061 .iter()
5062 .map(|t| t.name())
5063 .collect();
5064
5065 assert!(
5067 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5068 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5069 tool_names
5070 );
5071
5072 assert_eq!(
5074 collected.tools.len(),
5075 collected.tool_definitions.len(),
5076 "dependency-added tools must have implementations, not just definitions"
5077 );
5078 }
5079
5080 #[test]
5081 fn test_defaults_do_not_include_bash() {
5082 let registry = crate::ToolRegistry::with_defaults();
5085 assert!(
5086 !registry.has("bash"),
5087 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5088 );
5089 }
5090
5091 #[tokio::test]
5098 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5099 let registry = CapabilityRegistry::with_builtins();
5100 let collected =
5101 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5102
5103 let tool_names: Vec<&str> = collected
5104 .tool_definitions
5105 .iter()
5106 .map(|t| t.name())
5107 .collect();
5108 assert!(
5109 tool_names.contains(&"spawn_background"),
5110 "spawn_background must be auto-activated when bashkit_shell (a \
5111 background-capable tool) is in the agent's capability set; got: {:?}",
5112 tool_names
5113 );
5114 assert!(
5115 collected
5116 .applied_ids
5117 .iter()
5118 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5119 "background_execution must be in applied_ids when auto-activated; \
5120 got: {:?}",
5121 collected.applied_ids
5122 );
5123
5124 assert!(
5126 collected
5127 .tools
5128 .iter()
5129 .any(|t| t.name() == "spawn_background"),
5130 "spawn_background tool implementation must be present alongside the \
5131 definition (lockstep contract)"
5132 );
5133 }
5134
5135 #[tokio::test]
5138 async fn test_background_execution_does_not_auto_activate_without_hint() {
5139 let registry = CapabilityRegistry::with_builtins();
5140 let collected =
5142 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5143
5144 let tool_names: Vec<&str> = collected
5145 .tool_definitions
5146 .iter()
5147 .map(|t| t.name())
5148 .collect();
5149 assert!(
5150 !tool_names.contains(&"spawn_background"),
5151 "spawn_background must NOT be activated without a background-capable \
5152 tool; got: {:?}",
5153 tool_names
5154 );
5155 assert!(
5156 !collected
5157 .applied_ids
5158 .iter()
5159 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5160 "background_execution must not appear in applied_ids when no \
5161 background-capable tool is present; got: {:?}",
5162 collected.applied_ids
5163 );
5164 }
5165
5166 #[tokio::test]
5167 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5168 let registry = CapabilityRegistry::with_builtins();
5169 let collected = collect_capabilities(
5170 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5171 ®istry,
5172 &test_ctx(),
5173 )
5174 .await;
5175
5176 assert!(
5177 collected
5178 .tools
5179 .iter()
5180 .any(|tool| tool.name() == "spawn_agent"),
5181 "subagent-only sessions should get the unified spawn_agent adapter"
5182 );
5183 let spawn_agent = collected
5184 .tool_definitions
5185 .iter()
5186 .find(|tool| tool.name() == "spawn_agent")
5187 .expect("spawn_agent definition");
5188 assert_eq!(
5189 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5190 serde_json::json!(["subagent"])
5191 );
5192 }
5193
5194 #[tokio::test]
5195 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5196 let mut registry = CapabilityRegistry::new();
5197 registry.register(AgentHandoffCapability);
5198 let agent_id = crate::typed_id::AgentId::new();
5199 let harness_id = crate::typed_id::HarnessId::new();
5200 let configs = vec![AgentCapabilityConfig {
5201 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5202 config: serde_json::json!({
5203 "targets": [{
5204 "id": "aws_operator",
5205 "name": "AWS Operator",
5206 "agent_id": agent_id,
5207 "harness_id": harness_id
5208 }]
5209 }),
5210 }];
5211 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5212
5213 assert!(
5214 collected
5215 .tools
5216 .iter()
5217 .any(|tool| tool.name() == "spawn_agent"),
5218 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5219 );
5220 let spawn_agent = collected
5221 .tool_definitions
5222 .iter()
5223 .find(|tool| tool.name() == "spawn_agent")
5224 .expect("spawn_agent definition");
5225 assert_eq!(
5226 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5227 serde_json::json!(["agent"])
5228 );
5229 }
5230
5231 #[tokio::test]
5232 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5233 let mut registry = CapabilityRegistry::new();
5234 registry.register(SubagentCapability);
5235 registry.register(AgentHandoffCapability);
5236
5237 let agent_id = crate::typed_id::AgentId::new();
5238 let harness_id = crate::typed_id::HarnessId::new();
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(AGENT_HANDOFF_CAPABILITY_ID),
5246 config: serde_json::json!({
5247 "targets": [{
5248 "id": "aws_operator",
5249 "name": "AWS Operator",
5250 "agent_id": agent_id,
5251 "harness_id": harness_id
5252 }]
5253 }),
5254 },
5255 ];
5256
5257 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5258 let spawn_agent_defs: Vec<_> = collected
5259 .tool_definitions
5260 .iter()
5261 .filter(|tool| tool.name() == "spawn_agent")
5262 .collect();
5263
5264 assert_eq!(spawn_agent_defs.len(), 1);
5265 assert_eq!(
5266 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5267 serde_json::json!(["subagent", "agent"])
5268 );
5269 }
5270
5271 #[cfg(feature = "a2a")]
5272 #[tokio::test]
5273 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5274 let mut registry = CapabilityRegistry::new();
5275 registry.register(SubagentCapability);
5276 registry.register(A2aAgentDelegationCapability);
5277
5278 let configs = vec![
5279 AgentCapabilityConfig {
5280 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5281 config: serde_json::json!({}),
5282 },
5283 AgentCapabilityConfig {
5284 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5285 config: serde_json::json!({
5286 "agents": [{
5287 "id": "local_app",
5288 "name": "Local App",
5289 "base_url": "https://example.com"
5290 }]
5291 }),
5292 },
5293 ];
5294
5295 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5296 let spawn_agent_defs: Vec<_> = collected
5297 .tool_definitions
5298 .iter()
5299 .filter(|tool| tool.name() == "spawn_agent")
5300 .collect();
5301
5302 assert_eq!(spawn_agent_defs.len(), 1);
5303 assert_eq!(
5304 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5305 serde_json::json!(["subagent", "external_a2a"])
5306 );
5307 }
5308
5309 struct ExistingSpawnAgentCapability;
5310
5311 impl Capability for ExistingSpawnAgentCapability {
5312 fn id(&self) -> &str {
5313 "existing_spawn_agent"
5314 }
5315
5316 fn name(&self) -> &str {
5317 "Existing Spawn Agent"
5318 }
5319
5320 fn description(&self) -> &str {
5321 "Test capability that already owns spawn_agent"
5322 }
5323
5324 fn tools(&self) -> Vec<Box<dyn Tool>> {
5325 vec![Box::new(ExistingSpawnAgentTool)]
5326 }
5327 }
5328
5329 struct ExistingSpawnAgentTool;
5330
5331 #[async_trait]
5332 impl Tool for ExistingSpawnAgentTool {
5333 fn name(&self) -> &str {
5334 "spawn_agent"
5335 }
5336
5337 fn description(&self) -> &str {
5338 "Existing spawn_agent test tool"
5339 }
5340
5341 fn parameters_schema(&self) -> serde_json::Value {
5342 serde_json::json!({
5343 "type": "object",
5344 "properties": {
5345 "target": {
5346 "type": "object",
5347 "properties": {
5348 "type": {"type": "string", "enum": ["external_a2a"]}
5349 },
5350 "required": ["type"]
5351 }
5352 },
5353 "required": ["target"]
5354 })
5355 }
5356
5357 async fn execute(
5358 &self,
5359 _arguments: serde_json::Value,
5360 ) -> crate::tools::ToolExecutionResult {
5361 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5362 }
5363 }
5364
5365 #[tokio::test]
5366 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5367 let mut registry = CapabilityRegistry::new();
5368 registry.register(SubagentCapability);
5369 registry.register(ExistingSpawnAgentCapability);
5370
5371 let collected = collect_capabilities(
5372 &[
5373 SUBAGENTS_CAPABILITY_ID.to_string(),
5374 "existing_spawn_agent".to_string(),
5375 ],
5376 ®istry,
5377 &test_ctx(),
5378 )
5379 .await;
5380
5381 let spawn_agent_defs: Vec<_> = collected
5382 .tool_definitions
5383 .iter()
5384 .filter(|tool| tool.name() == "spawn_agent")
5385 .collect();
5386 assert_eq!(spawn_agent_defs.len(), 1);
5387 assert_eq!(
5388 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5389 serde_json::json!(["external_a2a"])
5390 );
5391 }
5392
5393 #[tokio::test]
5394 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5395 let mut registry = CapabilityRegistry::new();
5396 registry.register(AgentHandoffCapability);
5397 registry.register(ExistingSpawnAgentCapability);
5398
5399 let agent_id = crate::typed_id::AgentId::new();
5400 let harness_id = crate::typed_id::HarnessId::new();
5401 let configs = vec![
5402 AgentCapabilityConfig {
5403 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5404 config: serde_json::json!({
5405 "targets": [{
5406 "id": "aws_operator",
5407 "name": "AWS Operator",
5408 "agent_id": agent_id,
5409 "harness_id": harness_id
5410 }]
5411 }),
5412 },
5413 AgentCapabilityConfig {
5414 capability_ref: CapabilityId::new("existing_spawn_agent"),
5415 config: serde_json::json!({}),
5416 },
5417 ];
5418
5419 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5420
5421 let spawn_agent_defs: Vec<_> = collected
5422 .tool_definitions
5423 .iter()
5424 .filter(|tool| tool.name() == "spawn_agent")
5425 .collect();
5426 assert_eq!(spawn_agent_defs.len(), 1);
5427 assert_eq!(
5428 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5429 serde_json::json!(["external_a2a"])
5430 );
5431 }
5432
5433 #[tokio::test]
5437 async fn test_background_execution_explicit_selection_is_idempotent() {
5438 let registry = CapabilityRegistry::with_builtins();
5439 let collected = collect_capabilities(
5440 &[
5441 "bashkit_shell".to_string(),
5442 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5443 ],
5444 ®istry,
5445 &test_ctx(),
5446 )
5447 .await;
5448
5449 let spawn_background_count = collected
5450 .tool_definitions
5451 .iter()
5452 .filter(|t| t.name() == "spawn_background")
5453 .count();
5454 assert_eq!(
5455 spawn_background_count, 1,
5456 "spawn_background must appear exactly once even when \
5457 background_execution is selected explicitly alongside a \
5458 background-capable tool"
5459 );
5460 let applied_count = collected
5461 .applied_ids
5462 .iter()
5463 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5464 .count();
5465 assert_eq!(
5466 applied_count, 1,
5467 "background_execution must appear exactly once in applied_ids"
5468 );
5469 }
5470
5471 #[test]
5476 fn test_defaults_do_not_include_spawn_background() {
5477 let registry = crate::ToolRegistry::with_defaults();
5478 assert!(
5479 !registry.has("spawn_background"),
5480 "with_defaults() must not include 'spawn_background' — it comes \
5481 from the background_execution capability (EVE-501)"
5482 );
5483 }
5484
5485 #[test]
5490 fn test_capability_features_default_empty() {
5491 let registry = CapabilityRegistry::with_builtins();
5492
5493 let noop = registry.get("noop").unwrap();
5495 assert!(noop.features().is_empty());
5496
5497 let current_time = registry.get("current_time").unwrap();
5498 assert!(current_time.features().is_empty());
5499 }
5500
5501 #[test]
5502 fn test_file_system_capability_features() {
5503 let registry = CapabilityRegistry::with_builtins();
5504
5505 let fs = registry.get("session_file_system").unwrap();
5506 assert_eq!(fs.features(), vec!["file_system"]);
5507 }
5508
5509 #[test]
5510 fn test_bashkit_shell_capability_features() {
5511 let registry = CapabilityRegistry::with_builtins();
5512
5513 let bash = registry.get("bashkit_shell").unwrap();
5514 assert_eq!(bash.features(), vec!["file_system"]);
5515 }
5516
5517 #[test]
5518 fn test_alias_resolves_to_canonical_capability() {
5519 let registry = CapabilityRegistry::with_builtins();
5520
5521 let via_alias = registry.get("virtual_bash").unwrap();
5523 assert_eq!(via_alias.id(), "bashkit_shell");
5524 assert!(registry.has("virtual_bash"));
5525 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5526 assert_eq!(
5527 registry.canonical_id("bashkit_shell"),
5528 Some("bashkit_shell")
5529 );
5530 assert_eq!(registry.canonical_id("nonexistent"), None);
5531 }
5532
5533 #[test]
5534 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5535 let registry = CapabilityRegistry::with_builtins();
5536
5537 let resolved = resolve_dependencies(
5540 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5541 ®istry,
5542 )
5543 .unwrap();
5544 let bash_ids: Vec<_> = resolved
5545 .resolved_ids
5546 .iter()
5547 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5548 .collect();
5549 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5550 assert!(
5552 !resolved
5553 .added_as_dependencies
5554 .contains(&"bashkit_shell".to_string())
5555 );
5556 }
5557
5558 #[test]
5559 fn test_alias_preserves_explicit_config_in_resolution() {
5560 let registry = CapabilityRegistry::with_builtins();
5561
5562 let configs = vec![AgentCapabilityConfig::with_config(
5563 "virtual_bash".to_string(),
5564 serde_json::json!({"key": "value"}),
5565 )];
5566 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5567 let bash = resolved
5568 .iter()
5569 .find(|c| c.capability_id() == "bashkit_shell")
5570 .expect("alias must resolve to canonical bashkit_shell config");
5571 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5572 }
5573
5574 #[test]
5575 fn test_unregister_by_alias_removes_capability_and_aliases() {
5576 let mut registry = CapabilityRegistry::with_builtins();
5577
5578 assert!(registry.unregister("virtual_bash").is_some());
5579 assert!(!registry.has("bashkit_shell"));
5580 assert!(!registry.has("virtual_bash"));
5581 }
5582
5583 #[test]
5584 fn test_session_storage_capability_features() {
5585 let registry = CapabilityRegistry::with_builtins();
5586
5587 let storage = registry.get("session_storage").unwrap();
5588 let features = storage.features();
5589 assert!(features.contains(&"secrets"));
5590 assert!(features.contains(&"key_value"));
5591 }
5592
5593 #[test]
5594 fn test_session_schedule_capability_features() {
5595 let registry = CapabilityRegistry::with_builtins();
5596
5597 let schedule = registry.get("session_schedule").unwrap();
5598 assert_eq!(schedule.features(), vec!["schedules"]);
5599 }
5600
5601 #[test]
5602 fn test_session_sql_database_capability_features() {
5603 let registry = CapabilityRegistry::with_builtins();
5604
5605 let sql = registry.get("session_sql_database").unwrap();
5606 assert_eq!(sql.features(), vec!["sql_database"]);
5607 }
5608
5609 #[test]
5610 fn test_sample_data_capability_features() {
5611 let registry = CapabilityRegistry::with_builtins();
5612
5613 let sample = registry.get("sample_data").unwrap();
5614 assert_eq!(sample.features(), vec!["file_system"]);
5615 }
5616
5617 #[test]
5618 fn test_compute_features_empty() {
5619 let registry = CapabilityRegistry::with_builtins();
5620
5621 let features = compute_features(&[], ®istry);
5622 assert!(features.is_empty());
5623 }
5624
5625 #[test]
5626 fn test_compute_features_single_capability() {
5627 let registry = CapabilityRegistry::with_builtins();
5628
5629 let features = compute_features(&["session_schedule".to_string()], ®istry);
5630 assert_eq!(features, vec!["schedules"]);
5631 }
5632
5633 #[test]
5634 fn test_compute_features_multiple_capabilities() {
5635 let registry = CapabilityRegistry::with_builtins();
5636
5637 let features = compute_features(
5638 &[
5639 "session_file_system".to_string(),
5640 "session_storage".to_string(),
5641 "session_schedule".to_string(),
5642 ],
5643 ®istry,
5644 );
5645 assert!(features.contains(&"file_system".to_string()));
5646 assert!(features.contains(&"secrets".to_string()));
5647 assert!(features.contains(&"key_value".to_string()));
5648 assert!(features.contains(&"schedules".to_string()));
5649 }
5650
5651 #[test]
5652 fn test_compute_features_deduplicates() {
5653 let registry = CapabilityRegistry::with_builtins();
5654
5655 let features = compute_features(
5657 &[
5658 "session_file_system".to_string(),
5659 "bashkit_shell".to_string(),
5660 ],
5661 ®istry,
5662 );
5663 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5664 assert_eq!(file_system_count, 1, "file_system should appear only once");
5665 }
5666
5667 #[test]
5668 fn test_compute_features_includes_dependency_features() {
5669 let registry = CapabilityRegistry::with_builtins();
5670
5671 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5673 assert!(features.contains(&"file_system".to_string()));
5674 }
5675
5676 #[test]
5677 fn test_compute_features_generic_harness_set() {
5678 let registry = CapabilityRegistry::with_builtins();
5679
5680 let features = compute_features(
5682 &[
5683 "session_file_system".to_string(),
5684 "bashkit_shell".to_string(),
5685 "session_storage".to_string(),
5686 "session".to_string(),
5687 "session_schedule".to_string(),
5688 ],
5689 ®istry,
5690 );
5691 assert!(features.contains(&"file_system".to_string()));
5692 assert!(features.contains(&"secrets".to_string()));
5693 assert!(features.contains(&"key_value".to_string()));
5694 assert!(features.contains(&"schedules".to_string()));
5695 }
5696
5697 #[test]
5698 fn test_compute_features_unknown_capability_ignored() {
5699 let registry = CapabilityRegistry::with_builtins();
5700
5701 let features = compute_features(
5702 &["unknown_cap".to_string(), "session_schedule".to_string()],
5703 ®istry,
5704 );
5705 assert_eq!(features, vec!["schedules"]);
5706 }
5707
5708 #[test]
5709 fn test_risk_level_ordering() {
5710 assert!(RiskLevel::Low < RiskLevel::Medium);
5711 assert!(RiskLevel::Medium < RiskLevel::High);
5712 }
5713
5714 #[test]
5715 fn test_risk_level_serde_roundtrip() {
5716 let high = RiskLevel::High;
5717 let json = serde_json::to_string(&high).unwrap();
5718 assert_eq!(json, "\"high\"");
5719 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5720 assert_eq!(back, RiskLevel::High);
5721 }
5722
5723 #[test]
5724 fn test_capability_risk_levels() {
5725 let registry = CapabilityRegistry::with_builtins();
5726
5727 let bash = registry.get("bashkit_shell").unwrap();
5729 assert_eq!(bash.risk_level(), RiskLevel::High);
5730
5731 let fetch = registry.get("web_fetch").unwrap();
5733 assert_eq!(fetch.risk_level(), RiskLevel::High);
5734
5735 let noop = registry.get("noop").unwrap();
5737 assert_eq!(noop.risk_level(), RiskLevel::Low);
5738 }
5739
5740 #[tokio::test]
5745 async fn test_apply_capabilities_openai_tool_search() {
5746 let registry = CapabilityRegistry::with_builtins();
5747 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5748
5749 let applied = apply_capabilities(
5750 base_runtime_agent.clone(),
5751 &["openai_tool_search".to_string()],
5752 ®istry,
5753 &test_ctx(),
5754 )
5755 .await;
5756
5757 assert_eq!(
5759 applied.runtime_agent.system_prompt,
5760 base_runtime_agent.system_prompt
5761 );
5762 assert!(applied.tool_registry.is_empty());
5763 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
5764
5765 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5767 assert!(ts.enabled);
5768 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5769 }
5770
5771 #[tokio::test]
5772 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
5773 let registry = CapabilityRegistry::with_builtins();
5774 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5775
5776 let applied = apply_capabilities(
5777 base_runtime_agent,
5778 &[
5779 "current_time".to_string(),
5780 "openai_tool_search".to_string(),
5781 "test_math".to_string(),
5782 ],
5783 ®istry,
5784 &test_ctx(),
5785 )
5786 .await;
5787
5788 assert!(applied.tool_registry.has("get_current_time"));
5790 assert!(applied.tool_registry.has("add"));
5791 assert!(applied.tool_registry.has("subtract"));
5792 assert!(applied.tool_registry.has("multiply"));
5793 assert!(applied.tool_registry.has("divide"));
5794
5795 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5797 assert!(ts.enabled);
5798 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5799 }
5800
5801 #[tokio::test]
5802 async fn test_collect_capabilities_tool_search_custom_threshold() {
5803 let registry = CapabilityRegistry::with_builtins();
5804
5805 let configs = vec![AgentCapabilityConfig {
5806 capability_ref: CapabilityId::new("openai_tool_search"),
5807 config: serde_json::json!({"threshold": 5}),
5808 }];
5809
5810 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5811
5812 let ts = collected.tool_search.as_ref().unwrap();
5813 assert!(ts.enabled);
5814 assert_eq!(ts.threshold, 5);
5815 }
5816
5817 #[tokio::test]
5818 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
5819 let registry = CapabilityRegistry::with_builtins();
5820
5821 let configs = vec![
5822 AgentCapabilityConfig {
5823 capability_ref: CapabilityId::new("auto_tool_search"),
5824 config: serde_json::json!({"threshold": 2}),
5825 },
5826 AgentCapabilityConfig {
5827 capability_ref: CapabilityId::new("test_math"),
5828 config: serde_json::json!({}),
5829 },
5830 ];
5831
5832 let ctx = test_ctx().with_model("claude-3-5-haiku");
5836 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5837
5838 assert!(
5839 collected.tool_search.is_none(),
5840 "auto_tool_search must not set a hosted config on a non-native model"
5841 );
5842 assert!(
5843 collected
5844 .tools
5845 .iter()
5846 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5847 "auto_tool_search must contribute the client-side tool_search tool"
5848 );
5849 assert!(
5850 !collected.tool_definition_hooks.is_empty(),
5851 "auto_tool_search must contribute a client-side deferral hook"
5852 );
5853
5854 let mut transformed = collected.tool_definitions.clone();
5855 for hook in &collected.tool_definition_hooks {
5856 transformed = hook.transform(transformed);
5857 }
5858 let add_tool = transformed
5859 .iter()
5860 .find(|tool| tool.name() == "add")
5861 .expect("test_math contributes add");
5862 assert!(
5863 add_tool.parameters().get("properties").is_none(),
5864 "generic auto_tool_search must honor the configured threshold"
5865 );
5866 }
5867
5868 #[tokio::test]
5869 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
5870 let registry = CapabilityRegistry::with_builtins();
5871
5872 let configs = vec![AgentCapabilityConfig {
5873 capability_ref: CapabilityId::new("auto_tool_search"),
5874 config: serde_json::json!({"threshold": 7}),
5875 }];
5876
5877 let ctx = test_ctx().with_model("gpt-5.4");
5880 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5881
5882 let ts = collected
5883 .tool_search
5884 .as_ref()
5885 .expect("auto_tool_search must set a hosted config on a native model");
5886 assert!(ts.enabled);
5887 assert_eq!(ts.threshold, 7);
5888 assert!(
5889 !collected
5890 .tools
5891 .iter()
5892 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5893 "hosted mechanism must not contribute the client-side tool_search tool"
5894 );
5895 assert!(
5896 collected.tool_definition_hooks.is_empty(),
5897 "hosted mechanism must not contribute a client-side deferral hook"
5898 );
5899 }
5900
5901 #[tokio::test]
5902 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
5903 let registry = CapabilityRegistry::with_builtins();
5904
5905 let configs = vec![AgentCapabilityConfig {
5906 capability_ref: CapabilityId::new("auto_tool_search"),
5907 config: serde_json::json!({"threshold": 9}),
5908 }];
5909
5910 let ctx = test_ctx().with_model("claude-opus-4-8");
5913 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5914
5915 let ts = collected
5916 .tool_search
5917 .as_ref()
5918 .expect("auto_tool_search must set a hosted config on a native Claude model");
5919 assert!(ts.enabled);
5920 assert_eq!(ts.threshold, 9);
5921 assert!(
5922 !collected
5923 .tools
5924 .iter()
5925 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5926 "hosted mechanism must not contribute the client-side tool_search tool"
5927 );
5928 assert!(
5929 collected.tool_definition_hooks.is_empty(),
5930 "hosted mechanism must not contribute a client-side deferral hook"
5931 );
5932 }
5933
5934 #[tokio::test]
5935 async fn test_collect_capabilities_no_tool_search_without_capability() {
5936 let registry = CapabilityRegistry::with_builtins();
5937
5938 let configs = vec![AgentCapabilityConfig {
5939 capability_ref: CapabilityId::new("current_time"),
5940 config: serde_json::json!({}),
5941 }];
5942
5943 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5944
5945 assert!(collected.tool_search.is_none());
5946 }
5947
5948 #[tokio::test]
5949 async fn test_collect_capabilities_tool_search_category_propagation() {
5950 let registry = CapabilityRegistry::with_builtins();
5951
5952 let configs = vec![
5954 AgentCapabilityConfig {
5955 capability_ref: CapabilityId::new("test_math"),
5956 config: serde_json::json!({}),
5957 },
5958 AgentCapabilityConfig {
5959 capability_ref: CapabilityId::new("openai_tool_search"),
5960 config: serde_json::json!({}),
5961 },
5962 ];
5963
5964 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5965
5966 assert!(collected.tool_search.is_some());
5968
5969 for tool_def in &collected.tool_definitions {
5971 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
5973 assert!(
5974 tool_def.category().is_some(),
5975 "Tool {} should have a category from its capability",
5976 tool_def.name()
5977 );
5978 }
5979 }
5980 }
5981
5982 #[tokio::test]
5983 async fn test_apply_capabilities_prompt_caching() {
5984 let registry = CapabilityRegistry::with_builtins();
5985 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5986
5987 let applied = apply_capabilities(
5988 base_runtime_agent.clone(),
5989 &["prompt_caching".to_string()],
5990 ®istry,
5991 &test_ctx(),
5992 )
5993 .await;
5994
5995 assert_eq!(
5996 applied.runtime_agent.system_prompt,
5997 base_runtime_agent.system_prompt
5998 );
5999 assert!(applied.tool_registry.is_empty());
6000 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6001
6002 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6003 assert!(prompt_cache.enabled);
6004 assert_eq!(
6005 prompt_cache.strategy,
6006 crate::driver_registry::PromptCacheStrategy::Auto
6007 );
6008 assert!(prompt_cache.gemini_cached_content.is_none());
6009 }
6010
6011 #[tokio::test]
6012 async fn test_apply_capabilities_openrouter_server_tools() {
6013 let registry = CapabilityRegistry::with_builtins();
6014 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6015
6016 let configs = vec![AgentCapabilityConfig {
6017 capability_ref: CapabilityId::new("openrouter_server_tools"),
6018 config: serde_json::json!({
6019 "tools": ["web_search", "datetime"],
6020 "web_search_max_results": 4,
6021 }),
6022 }];
6023
6024 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6025 let routing = collected
6026 .openrouter_routing
6027 .as_ref()
6028 .expect("server tools produce routing config");
6029 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6030 assert_eq!(
6031 kinds,
6032 vec![
6033 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6034 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6035 ]
6036 );
6037
6038 let applied = apply_capabilities(
6041 base_runtime_agent,
6042 &["openrouter_server_tools".to_string()],
6043 ®istry,
6044 &test_ctx(),
6045 )
6046 .await;
6047 assert!(applied.tool_registry.is_empty());
6048 assert!(applied.runtime_agent.openrouter_routing.is_none());
6049 }
6050
6051 #[tokio::test]
6052 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6053 let registry = CapabilityRegistry::with_builtins();
6054
6055 let configs = vec![AgentCapabilityConfig {
6056 capability_ref: CapabilityId::new("prompt_caching"),
6057 config: serde_json::json!({"strategy": "auto"}),
6058 }];
6059
6060 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6061
6062 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6063 assert!(prompt_cache.enabled);
6064 assert_eq!(
6065 prompt_cache.strategy,
6066 crate::driver_registry::PromptCacheStrategy::Auto
6067 );
6068 assert!(prompt_cache.gemini_cached_content.is_none());
6069 }
6070
6071 #[tokio::test]
6072 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6073 let registry = CapabilityRegistry::with_builtins();
6074
6075 let configs = vec![AgentCapabilityConfig {
6076 capability_ref: CapabilityId::new("prompt_caching"),
6077 config: serde_json::json!({
6078 "strategy": "auto",
6079 "gemini_cached_content": "cachedContents/demo-cache"
6080 }),
6081 }];
6082
6083 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6084
6085 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6086 assert_eq!(
6087 prompt_cache.gemini_cached_content.as_deref(),
6088 Some("cachedContents/demo-cache")
6089 );
6090 }
6091
6092 #[tokio::test]
6093 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6094 let registry = CapabilityRegistry::with_builtins();
6095
6096 let collected = collect_capabilities_with_configs(
6098 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6099 ®istry,
6100 &test_ctx(),
6101 )
6102 .await;
6103 assert_eq!(collected.parallel_tool_calls, Some(true));
6104
6105 let collected = collect_capabilities_with_configs(
6107 &[AgentCapabilityConfig {
6108 capability_ref: CapabilityId::new("parallel_tool_calls"),
6109 config: serde_json::json!({"mode": "avoid"}),
6110 }],
6111 ®istry,
6112 &test_ctx(),
6113 )
6114 .await;
6115 assert_eq!(collected.parallel_tool_calls, Some(false));
6116
6117 let collected = collect_capabilities_with_configs(
6119 &[AgentCapabilityConfig {
6120 capability_ref: CapabilityId::new("parallel_tool_calls"),
6121 config: serde_json::json!({"mode": "none"}),
6122 }],
6123 ®istry,
6124 &test_ctx(),
6125 )
6126 .await;
6127 assert_eq!(collected.parallel_tool_calls, None);
6128
6129 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6131 assert_eq!(collected.parallel_tool_calls, None);
6132 }
6133
6134 #[tokio::test]
6135 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6136 let registry = CapabilityRegistry::with_builtins();
6137
6138 let applied = apply_capabilities(
6140 RuntimeAgent::new("p", "gpt-5.2"),
6141 &["parallel_tool_calls".to_string()],
6142 ®istry,
6143 &test_ctx(),
6144 )
6145 .await;
6146 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6147
6148 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6150 base.parallel_tool_calls = Some(false);
6151 let applied = apply_capabilities(
6152 base,
6153 &["parallel_tool_calls".to_string()],
6154 ®istry,
6155 &test_ctx(),
6156 )
6157 .await;
6158 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6159 }
6160
6161 struct SkillContributingCapability;
6166
6167 impl Capability for SkillContributingCapability {
6168 fn id(&self) -> &str {
6169 "contributes_skills"
6170 }
6171 fn name(&self) -> &str {
6172 "Contributes Skills"
6173 }
6174 fn description(&self) -> &str {
6175 "Test capability that contributes skills."
6176 }
6177 fn contribute_skills(&self) -> Vec<SkillContribution> {
6178 vec![
6179 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6180 .with_files(vec![(
6181 "scripts/a.sh".to_string(),
6182 "#!/bin/sh\necho a\n".to_string(),
6183 )]),
6184 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6185 .with_user_invocable(false),
6186 ]
6187 }
6188 }
6189
6190 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6191 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6192 MountSource::InlineFile { content, .. } => content.as_str(),
6193 _ => panic!("Expected InlineFile for SKILL.md"),
6194 }
6195 }
6196
6197 #[tokio::test]
6198 async fn test_contribute_skills_normalized_to_mounts() {
6199 let mut registry = CapabilityRegistry::new();
6200 registry.register(SkillContributingCapability);
6201
6202 let configs = vec![AgentCapabilityConfig {
6203 capability_ref: CapabilityId::new("contributes_skills"),
6204 config: serde_json::json!({}),
6205 }];
6206
6207 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6208
6209 let skill_mounts: Vec<_> = collected
6210 .mounts
6211 .iter()
6212 .filter(|m| m.path.starts_with("/.agents/skills/"))
6213 .collect();
6214 assert_eq!(skill_mounts.len(), 2);
6215
6216 for m in &skill_mounts {
6219 assert!(m.is_readonly());
6220 assert_eq!(m.capability_id, "contributes_skills");
6221 }
6222
6223 let alpha = skill_mounts
6224 .iter()
6225 .find(|m| m.path == "/.agents/skills/alpha-skill")
6226 .expect("alpha-skill mount missing");
6227 match &alpha.source {
6228 MountSource::InlineDirectory { entries } => {
6229 assert!(entries.contains_key("SKILL.md"));
6230 assert!(entries.contains_key("scripts/a.sh"));
6231 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6232 assert_eq!(parsed.name, "alpha-skill");
6233 assert!(parsed.user_invocable);
6234 }
6235 _ => panic!("Expected InlineDirectory"),
6236 }
6237
6238 let beta = skill_mounts
6239 .iter()
6240 .find(|m| m.path == "/.agents/skills/beta-skill")
6241 .expect("beta-skill mount missing");
6242 match &beta.source {
6243 MountSource::InlineDirectory { entries } => {
6244 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6245 assert!(!parsed.user_invocable);
6246 }
6247 _ => panic!("Expected InlineDirectory"),
6248 }
6249 }
6250
6251 #[tokio::test]
6252 async fn test_contribute_skills_default_empty() {
6253 let mut registry = CapabilityRegistry::new();
6256 registry.register(FilterTestCapability { priority: 0 });
6257
6258 let configs = vec![AgentCapabilityConfig {
6259 capability_ref: CapabilityId::new("filter_test"),
6260 config: serde_json::json!({}),
6261 }];
6262
6263 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6264 assert!(
6265 collected
6266 .mounts
6267 .iter()
6268 .all(|m| !m.path.starts_with("/.agents/skills/"))
6269 );
6270 }
6271
6272 struct LocalizedCapability;
6273
6274 impl Capability for LocalizedCapability {
6275 fn id(&self) -> &str {
6276 "localized"
6277 }
6278 fn name(&self) -> &str {
6279 "Localized"
6280 }
6281 fn description(&self) -> &str {
6282 "English description"
6283 }
6284 fn localizations(&self) -> Vec<CapabilityLocalization> {
6285 vec![
6286 CapabilityLocalization {
6287 locale: "en",
6288 name: None,
6289 description: None,
6290 config_description: Some("Controls things."),
6291 config_overlay: None,
6292 },
6293 CapabilityLocalization {
6294 locale: "uk",
6295 name: Some("Локалізована"),
6296 description: Some("Український опис"),
6297 config_description: Some("Керує налаштуваннями."),
6298 config_overlay: None,
6299 },
6300 ]
6301 }
6302 }
6303
6304 #[test]
6305 fn localized_name_falls_back_exact_language_then_base() {
6306 let cap = LocalizedCapability;
6307 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6309 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6310 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6312 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6314 assert_eq!(cap.localized_name(None), "Localized");
6315 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6316 assert_eq!(cap.localized_description(Some("de")), "English description");
6317 }
6318
6319 #[test]
6320 fn describe_schema_resolves_config_description_per_locale() {
6321 let cap = LocalizedCapability;
6322 assert_eq!(
6323 cap.describe_schema(Some("uk-UA")).as_deref(),
6324 Some("Керує налаштуваннями.")
6325 );
6326 assert_eq!(
6328 cap.describe_schema(Some("pl")).as_deref(),
6329 Some("Controls things.")
6330 );
6331 assert_eq!(
6332 cap.describe_schema(None).as_deref(),
6333 Some("Controls things.")
6334 );
6335 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6337 }
6338}