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 citation_retrieval;
98mod citation_verification;
99mod claude_tool_search;
100pub mod compaction;
101mod current_time;
102mod data_knowledge;
103mod declarative;
104mod delegation_result;
105mod error_disclosure;
106pub mod facts;
107mod fake_aws;
108mod fake_crm;
109mod fake_financial;
110mod fake_warehouse;
111mod file_system;
112mod guardrails;
113mod human_intent;
114mod infinity_context;
115mod knowledge_base;
116mod knowledge_index;
117mod loop_detection;
118mod lua;
119mod lua_code_mode;
120pub mod mcp;
121mod memory;
122mod message_metadata;
123mod model_scout;
124mod monitors;
125mod noop;
126mod openai_tool_search;
127mod openrouter_server_tools;
128mod openrouter_workspace;
129#[cfg(feature = "ui-capabilities")]
130mod openui;
131mod parallel_tool_calls;
132mod progress_guard;
133mod prompt_caching;
134mod prompt_canary_guardrail;
135mod research;
136mod sample_data;
137mod self_budget;
138mod session;
139mod session_sandbox;
140mod session_schedule;
141mod session_sql_database;
142mod session_storage;
143mod session_tasks;
144mod skills;
145mod skills_scoped;
146mod stateless_todo_list;
147mod subagents;
148mod system_commands;
149mod test_math;
150mod test_weather;
151mod tool_approval;
152mod tool_call_repair;
153mod tool_output_distillation;
154mod tool_output_persistence;
155mod tool_search;
156mod usage_limit_auto_continue;
157pub mod user_hooks;
158pub mod util;
159#[cfg(feature = "web-fetch")]
160mod web_fetch;
161
162pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
167pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
171#[cfg(feature = "a2a")]
172pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
173#[cfg(feature = "ui-capabilities")]
174pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
175pub use agent_handoff::{
176 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
177};
178pub use agent_instructions::{
179 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
180 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
181 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
182};
183pub use attach_skill::{
184 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
185 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
186 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
187};
188pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
189pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
190pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
191pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
192pub use citation_retrieval::{
193 CITATION_RETRIEVAL_CAPABILITY_ID, CitationRetrievalCapability, CitationRetrievalConfig,
194};
195pub use citation_verification::{
196 CITATION_VERIFICATION_CAPABILITY_ID, CitationVerificationCapability,
197 CitationVerificationConfig, VerificationMode,
198};
199pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
200pub use compaction::{
201 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
202 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
203 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
204 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
205 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
206 build_summarization_prompt, build_summary_message, classify_memory_tiers,
207 compose_summary_with_recent, estimate_tokens, estimate_total_tokens,
208 format_messages_for_summarization, should_compact_for_cost, should_compact_proactively,
209 total_tool_result_bytes,
210};
211pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
212pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
213pub use declarative::{
214 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
215 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
216 declarative_capability_info, hydrate_declarative_capability_config,
217 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
218 plugin_capability_info, validate_declarative_capability_definition,
219};
220pub use delegation_result::{
221 ReportResultTool, ReportTaskProgressTool, report_result_tool_for_child_session,
222 report_task_progress_tool_for_child_session,
223};
224pub use error_disclosure::{
225 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
226};
227pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
228pub use fake_aws::{
229 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
230 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
231 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
232 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
233};
234pub use fake_crm::{
235 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
236 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
237 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
238};
239pub use fake_financial::{
240 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
241 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
242 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
243 FinanceListTransactionsTool,
244};
245pub use fake_warehouse::{
246 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
247 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
248 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
249 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
250};
251pub use file_system::{
252 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
253 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
254};
255pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
256pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
257pub use infinity_context::{
258 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, InfinityContextFilterOnlyCapability,
259 QueryHistoryTool,
260};
261pub use knowledge_base::{
262 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
263 validate_knowledge_base_config,
264};
265pub use knowledge_index::{
266 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
267 validate_knowledge_index_config,
268};
269pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
270pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
271pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
272pub use mcp::{
273 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
274 parse_mcp_capability_id,
275};
276pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
277pub use message_metadata::{
278 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
279 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
280};
281pub use model_scout::{
282 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
283 RouterUpdateProposal, compute_score, rank_results,
284};
285pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
286pub use openai_tool_search::{
287 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
288 model_supports_native_tool_search,
289};
290pub use openrouter_server_tools::{
291 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
292};
293pub use openrouter_workspace::{
294 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
295 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
296 detect_policy_drift,
297};
298#[cfg(feature = "ui-capabilities")]
299pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
300pub use parallel_tool_calls::{
301 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
302 parallel_tool_calls_from_config,
303};
304pub use progress_guard::{PROGRESS_GUARD_CAPABILITY_ID, ProgressGuardCapability};
305pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
306pub use prompt_canary_guardrail::{
307 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
308 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
309 REASON_CODE_SYSTEM_PROMPT_LEAK,
310};
311pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
312pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
313pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
314pub use session::{
315 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, SessionCapabilityConfig,
316 SessionTitleMutation, WriteSessionTitleTool, session_title_updated_event,
317 update_session_title_with_event,
318};
319pub use session_sandbox::{
320 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
321 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
322};
323pub use session_schedule::{
324 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
325 SessionScheduleCapability,
326};
327pub use session_sql_database::{
328 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
329 SqlSchemaTool,
330};
331pub use session_storage::{
332 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
333 is_internal_session_kv_key, is_internal_session_secret_name,
334};
335pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
336pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
337pub use skills_scoped::{
338 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
339};
340pub use stateless_todo_list::{
341 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
342};
343pub(crate) use subagents::SPAWN_AGENT_CONCURRENCY_CLASS;
344pub use subagents::{SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool, SubagentCapability};
345pub use usage_limit_auto_continue::{
346 AutoContinueConfig, USAGE_LIMIT_AUTO_CONTINUE_CAPABILITY_ID, UsageLimitAutoContinueCapability,
347 resolve_usage_limit_auto_continue,
348};
349pub use bashkit_shell::{
351 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
352};
353pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
354pub use test_math::{
355 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
356};
357pub use test_weather::{
358 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
359};
360pub use tool_approval::{
361 ApprovalDecision, ApprovalMode, TOOL_APPROVAL_CAPABILITY_ID, ToolApprovalCapability,
362 ToolApprover,
363};
364pub use tool_call_repair::{
365 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
366 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
367 salvage_tool_arguments, tool_call_repair_capability,
368};
369pub use tool_output_distillation::{
370 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
371};
372pub use tool_output_persistence::{
373 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
374};
375pub use tool_search::{
376 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
377};
378pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
379#[cfg(feature = "web-fetch")]
380pub use web_fetch::{
381 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
382 derive_bot_auth_public_key,
383};
384
385pub struct SystemPromptContext {
395 pub session_id: SessionId,
397 pub locale: Option<String>,
399 pub file_store: Option<Arc<dyn SessionFileSystem>>,
401 pub model: Option<String>,
407}
408
409impl SystemPromptContext {
410 pub fn without_file_store(session_id: SessionId) -> Self {
412 Self {
413 session_id,
414 locale: None,
415 file_store: None,
416 model: None,
417 }
418 }
419
420 pub fn with_model(mut self, model: impl Into<String>) -> Self {
422 self.model = Some(model.into());
423 self
424 }
425}
426
427#[derive(Debug, Clone)]
479pub struct CapabilityLocalization {
480 pub locale: &'static str,
482 pub name: Option<&'static str>,
484 pub description: Option<&'static str>,
486 pub config_description: Option<&'static str>,
491 pub config_overlay: Option<serde_json::Value>,
497}
498
499impl CapabilityLocalization {
500 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
502 Self {
503 locale,
504 name: Some(name),
505 description: Some(description),
506 config_description: None,
507 config_overlay: None,
508 }
509 }
510}
511
512pub fn resolve_localized_field<T>(
516 localizations: &[CapabilityLocalization],
517 locale: Option<&str>,
518 field: impl Fn(&CapabilityLocalization) -> Option<T>,
519) -> Option<T> {
520 let mut candidates: Vec<String> = Vec::new();
521 if let Some(raw) = locale {
522 let normalized = raw.trim().replace('_', "-").to_lowercase();
523 if !normalized.is_empty() {
524 if let Some((language, _)) = normalized.split_once('-') {
525 let language = language.to_string();
526 candidates.push(normalized);
527 candidates.push(language);
528 } else {
529 candidates.push(normalized);
530 }
531 }
532 }
533 candidates.push("en".to_string());
534
535 for candidate in candidates {
536 let hit = localizations
537 .iter()
538 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
539 .and_then(&field);
540 if hit.is_some() {
541 return hit;
542 }
543 }
544 None
545}
546
547#[async_trait]
548pub trait Capability: Send + Sync {
549 fn id(&self) -> &str;
551
552 fn aliases(&self) -> Vec<&'static str> {
561 vec![]
562 }
563
564 fn name(&self) -> &str;
566
567 fn description(&self) -> &str;
569
570 fn localizations(&self) -> Vec<CapabilityLocalization> {
575 vec![]
576 }
577
578 fn localized_name(&self, locale: Option<&str>) -> String {
581 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
582 .unwrap_or_else(|| self.name())
583 .to_string()
584 }
585
586 fn localized_description(&self, locale: Option<&str>) -> String {
588 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
589 .unwrap_or_else(|| self.description())
590 .to_string()
591 }
592
593 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
597 resolve_localized_field(&self.localizations(), locale, |entry| {
598 entry.config_description
599 })
600 .map(str::to_string)
601 }
602
603 fn status(&self) -> CapabilityStatus {
605 CapabilityStatus::Available
606 }
607
608 fn icon(&self) -> Option<&str> {
610 None
611 }
612
613 fn category(&self) -> Option<&str> {
615 None
616 }
617
618 fn metadata(&self) -> Option<serde_json::Value> {
630 None
631 }
632
633 fn is_guardrail(&self) -> bool {
638 false
639 }
640
641 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
652 None
653 }
654
655 fn system_prompt_addition(&self) -> Option<&str> {
675 None
676 }
677
678 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
690 self.system_prompt_addition().map(|addition| {
691 format!(
692 "<capability id=\"{}\">\n{}\n</capability>",
693 self.id(),
694 addition
695 )
696 })
697 }
698
699 fn system_prompt_preview(&self) -> Option<String> {
705 self.system_prompt_addition().map(|s| s.to_string())
706 }
707
708 fn tools(&self) -> Vec<Box<dyn Tool>> {
710 vec![]
711 }
712
713 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
721 self.tools()
722 }
723
724 async fn system_prompt_contribution_with_config(
731 &self,
732 ctx: &SystemPromptContext,
733 _config: &serde_json::Value,
734 ) -> Option<String> {
735 self.system_prompt_contribution(ctx).await
736 }
737
738 fn tool_definitions(&self) -> Vec<ToolDefinition> {
741 self.tools().iter().map(|t| t.to_definition()).collect()
742 }
743
744 fn mounts(&self) -> Vec<MountPoint> {
752 vec![]
753 }
754
755 fn dependencies(&self) -> Vec<&'static str> {
764 vec![]
765 }
766
767 fn features(&self) -> Vec<&'static str> {
782 vec![]
783 }
784
785 fn config_schema(&self) -> Option<serde_json::Value> {
791 None
792 }
793
794 fn config_ui_schema(&self) -> Option<serde_json::Value> {
799 None
800 }
801
802 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
808 Ok(())
809 }
810
811 fn mcp_servers(&self) -> ScopedMcpServers {
817 ScopedMcpServers::default()
818 }
819
820 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
822 self.mcp_servers()
823 }
824
825 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
838 None
839 }
840
841 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
849 None
850 }
851
852 fn llm_error_hook(&self) -> Option<Arc<dyn crate::llm_error_hook::LlmErrorHook>> {
864 None
865 }
866
867 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
882 vec![]
883 }
884
885 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
896 vec![]
897 }
898
899 fn pre_tool_use_hooks_with_config(
904 &self,
905 _config: &serde_json::Value,
906 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
907 self.pre_tool_use_hooks()
908 }
909
910 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
918 vec![]
919 }
920
921 fn post_tool_exec_hooks_with_config(
926 &self,
927 _config: &serde_json::Value,
928 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
929 self.post_tool_exec_hooks()
930 }
931
932 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
941 vec![]
942 }
943
944 fn tool_definition_hooks_with_config(
949 &self,
950 _config: &serde_json::Value,
951 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
952 self.tool_definition_hooks()
953 }
954
955 fn tool_definition_hooks_with_context(
965 &self,
966 _ctx: &SystemPromptContext,
967 config: &serde_json::Value,
968 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
969 self.tool_definition_hooks_with_config(config)
970 }
971
972 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
980 vec![]
981 }
982
983 fn narrate(
997 &self,
998 _tool_def: Option<&ToolDefinition>,
999 tool_call: &ToolCall,
1000 phase: crate::tool_narration::ToolNarrationPhase,
1001 locale: Option<&str>,
1002 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1003 ) -> Option<String> {
1004 self.tools()
1005 .iter()
1006 .find(|tool| tool.name() == tool_call.name)
1007 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1008 }
1009
1010 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
1026 vec![]
1027 }
1028
1029 fn user_hooks_with_config(
1035 &self,
1036 _config: &serde_json::Value,
1037 ) -> Vec<crate::user_hook_types::UserHookSpec> {
1038 self.user_hooks()
1039 }
1040
1041 fn risk_level(&self) -> RiskLevel {
1049 RiskLevel::Low
1050 }
1051
1052 fn commands(&self) -> Vec<CommandDescriptor> {
1060 vec![]
1061 }
1062
1063 async fn execute_command(
1077 &self,
1078 request: &ExecuteCommandRequest,
1079 _ctx: &CommandExecutionContext,
1080 ) -> crate::error::Result<CommandResult> {
1081 Err(crate::error::AgentLoopError::config(format!(
1082 "capability {} declared command /{} but does not implement execute_command",
1083 self.id(),
1084 request.name,
1085 )))
1086 }
1087
1088 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1097 vec![]
1098 }
1099
1100 fn contribute_skills(&self) -> Vec<SkillContribution> {
1110 vec![]
1111 }
1112
1113 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1124 vec![]
1125 }
1126
1127 fn post_output_guardrails_with_config(
1139 &self,
1140 _config: &serde_json::Value,
1141 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1142 vec![]
1143 }
1144
1145 fn post_output_annotation_hooks_with_config(
1161 &self,
1162 _config: &serde_json::Value,
1163 ) -> Vec<Arc<dyn crate::annotation_hook::PostGenerationAnnotationHook>> {
1164 vec![]
1165 }
1166
1167 fn citation_verifier_with_config(
1177 &self,
1178 _config: &serde_json::Value,
1179 ) -> Option<Arc<dyn crate::annotation_hook::CitationVerifier>> {
1180 None
1181 }
1182}
1183
1184pub trait ToolDefinitionHook: Send + Sync {
1185 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1186
1187 fn applies_with_native_tool_search(&self) -> bool {
1192 true
1193 }
1194}
1195
1196pub trait ToolCallHook: Send + Sync {
1197 fn narration(
1198 &self,
1199 _tool_def: Option<&ToolDefinition>,
1200 _tool_call: &ToolCall,
1201 _phase: crate::tool_narration::ToolNarrationPhase,
1202 _locale: Option<&str>,
1203 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
1204 ) -> Option<String> {
1205 None
1206 }
1207
1208 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1209 tool_call
1210 }
1211}
1212
1213pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1219
1220impl ToolCallHook for CapabilityNarrationHook {
1221 fn narration(
1222 &self,
1223 tool_def: Option<&ToolDefinition>,
1224 tool_call: &ToolCall,
1225 phase: crate::tool_narration::ToolNarrationPhase,
1226 locale: Option<&str>,
1227 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1228 ) -> Option<String> {
1229 self.0.narrate(tool_def, tool_call, phase, locale, ctx)
1230 }
1231}
1232
1233#[derive(
1237 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1238)]
1239#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1240#[cfg_attr(feature = "openapi", schema(example = "low"))]
1241#[serde(rename_all = "lowercase")]
1242pub enum RiskLevel {
1243 Low,
1245 Medium,
1247 High,
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1257#[serde(rename_all = "snake_case")]
1258pub enum BlueprintModel {
1259 Fixed(String),
1261 Default(String),
1263 Inherit,
1265}
1266
1267pub struct AgentBlueprint {
1273 pub id: &'static str,
1275 pub name: &'static str,
1277 pub description: &'static str,
1279 pub model: BlueprintModel,
1281 pub system_prompt: &'static str,
1283 pub tools: Vec<Box<dyn Tool>>,
1285 pub max_turns: Option<usize>,
1287 pub config_schema: Option<serde_json::Value>,
1289}
1290
1291impl AgentBlueprint {
1292 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1294 self.tools.iter().map(|t| t.to_definition()).collect()
1295 }
1296}
1297
1298impl std::fmt::Debug for AgentBlueprint {
1299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1300 f.debug_struct("AgentBlueprint")
1301 .field("id", &self.id)
1302 .field("name", &self.name)
1303 .field("model", &self.model)
1304 .field("tool_count", &self.tools.len())
1305 .field("max_turns", &self.max_turns)
1306 .finish()
1307 }
1308}
1309
1310#[derive(Clone)]
1337pub struct CapabilityRegistry {
1338 capabilities: HashMap<String, Arc<dyn Capability>>,
1339 aliases: HashMap<String, String>,
1341}
1342
1343impl CapabilityRegistry {
1344 pub fn new() -> Self {
1346 Self {
1347 capabilities: HashMap::new(),
1348 aliases: HashMap::new(),
1349 }
1350 }
1351
1352 pub fn with_builtins() -> Self {
1357 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1358 }
1359
1360 pub fn runtime_builtins() -> Self {
1371 let mut registry = Self::new();
1372
1373 registry.register(AgentInstructionsCapability);
1374 registry.register(HumanIntentCapability);
1375 registry.register(NoopCapability);
1376 registry.register(CurrentTimeCapability);
1377 registry.register(MessageMetadataCapability);
1378 registry.register(FileSystemCapability);
1379 registry.register(SessionStorageCapability);
1380 registry.register(SessionCapability);
1381 registry.register(StatelessTodoListCapability);
1382 #[cfg(feature = "web-fetch")]
1383 registry.register(WebFetchCapability::from_env());
1384 registry.register(BashkitShellCapability);
1385 registry.register(BtwCapability);
1386 registry.register(InfinityContextCapability);
1387 registry.register(budgeting::BudgetingCapability);
1388 registry.register(SelfBudgetCapability);
1389 registry.register(CompactionCapability);
1390 registry.register(ErrorDisclosureCapability);
1391 registry.register(OpenAiToolSearchCapability::new());
1392 registry.register(ClaudeToolSearchCapability::new());
1393 registry.register(ToolSearchCapability::new());
1394 registry.register(AutoToolSearchCapability::new());
1395 registry.register(PromptCachingCapability::new());
1396 registry.register(ParallelToolCallsCapability);
1397 registry.register(SkillsCapability);
1398 registry.register(SystemCommandsCapability);
1399 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1400 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1401 registry.register(LoopDetectionCapability);
1402 registry.register(ProgressGuardCapability::new());
1403 registry.register(ToolCallRepairCapability);
1404 registry.register(PromptCanaryGuardrailCapability);
1405 registry.register(GuardrailsCapability);
1406 registry.register(user_hooks::UserHooksCapability);
1407
1408 let internal_flags = crate::InternalFeatureFlags::from_env();
1409 if internal_flags.lua {
1410 registry.register(LuaCapability);
1411 registry.register(LuaCodeModeCapability);
1412 }
1413
1414 registry
1415 }
1416
1417 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1422 let mut registry = Self::new();
1423
1424 registry.register(AgentInstructionsCapability);
1426 registry.register(HumanIntentCapability);
1427 registry.register(NoopCapability);
1428 registry.register(CurrentTimeCapability);
1429 registry.register(MessageMetadataCapability);
1430 registry.register(ResearchCapability);
1431 registry.register(ModelScoutCapability);
1432 registry.register(OpenRouterWorkspaceCapability);
1433 registry.register(OpenRouterServerToolsCapability);
1434 registry.register(FileSystemCapability);
1435 registry.register(MemoryCapability);
1436 registry.register(SessionStorageCapability);
1437 registry.register(SessionCapability);
1438 registry.register(SessionSqlDatabaseCapability);
1439 registry.register(TestMathCapability);
1440 registry.register(TestWeatherCapability);
1441 registry.register(StatelessTodoListCapability);
1442 #[cfg(feature = "web-fetch")]
1443 registry.register(WebFetchCapability::from_env());
1444 registry.register(BashkitShellCapability);
1445 registry.register(BackgroundExecutionCapability);
1446 registry.register(SessionScheduleCapability);
1447 registry.register(BtwCapability);
1448 registry.register(InfinityContextCapability);
1449 registry.register(budgeting::BudgetingCapability);
1450 registry.register(SelfBudgetCapability);
1451 registry.register(CompactionCapability);
1452 registry.register(ErrorDisclosureCapability);
1453
1454 registry.register(OpenAiToolSearchCapability::new());
1456 registry.register(ClaudeToolSearchCapability::new());
1458 registry.register(ToolSearchCapability::new());
1460 registry.register(AutoToolSearchCapability::new());
1462 registry.register(PromptCachingCapability::new());
1463
1464 registry.register(ParallelToolCallsCapability);
1466
1467 registry.register(SkillsCapability);
1469
1470 registry.register(SubagentCapability);
1472
1473 registry.register(SessionTasksCapability);
1475
1476 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1480 registry.register(AgentHandoffCapability);
1481 #[cfg(feature = "a2a")]
1485 registry.register(A2aAgentDelegationCapability);
1486 }
1487
1488 registry.register(SystemCommandsCapability);
1490
1491 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1493 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1494
1495 registry.register(user_hooks::UserHooksCapability);
1498
1499 registry.register(LoopDetectionCapability);
1501
1502 registry.register(ProgressGuardCapability::new());
1506
1507 registry.register(UsageLimitAutoContinueCapability);
1514
1515 registry.register(ToolCallRepairCapability);
1519
1520 registry.register(PromptCanaryGuardrailCapability);
1523
1524 registry.register(GuardrailsCapability);
1527
1528 #[cfg(feature = "ui-capabilities")]
1530 {
1531 registry.register(OpenUiCapability);
1532 registry.register(A2UiCapability);
1533 }
1534
1535 registry.register(SampleDataCapability);
1537
1538 registry.register(DataKnowledgeCapability);
1540
1541 registry.register(KnowledgeBaseCapability);
1543
1544 registry.register(KnowledgeIndexCapability);
1546
1547 registry.register(CitationRetrievalCapability);
1549
1550 registry.register(CitationVerificationCapability);
1552
1553 registry.register(FakeWarehouseCapability);
1555 registry.register(FakeAwsCapability);
1556 registry.register(FakeCrmCapability);
1557 registry.register(FakeFinancialCapability);
1558
1559 let internal_flags = crate::InternalFeatureFlags::from_env();
1561 if internal_flags.session_sandbox {
1562 registry.register(SessionSandboxCapability);
1563 }
1564
1565 if internal_flags.lua {
1569 registry.register(LuaCapability);
1570 registry.register(LuaCodeModeCapability);
1573 }
1574 for plugin in inventory::iter::<IntegrationPlugin>() {
1575 if (!plugin.experimental_only || grade.experimental_features_enabled())
1576 && plugin
1577 .feature_flag
1578 .is_none_or(|f| internal_flags.is_enabled(f))
1579 {
1580 registry.register_boxed((plugin.factory)());
1581 }
1582 }
1583
1584 registry
1585 }
1586
1587 pub fn register(&mut self, capability: impl Capability + 'static) {
1589 self.register_arc(Arc::new(capability));
1590 }
1591
1592 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1594 self.register_arc(Arc::from(capability));
1595 }
1596
1597 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1599 let canonical = capability.id().to_string();
1600 for alias in capability.aliases() {
1601 self.aliases.insert(alias.to_string(), canonical.clone());
1602 }
1603 self.capabilities.insert(canonical, capability);
1604 }
1605
1606 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1608 self.capabilities
1609 .get(id)
1610 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1611 }
1612
1613 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1618 if self.capabilities.contains_key(id) {
1619 Some(id)
1620 } else {
1621 self.aliases
1622 .get(id)
1623 .filter(|c| self.capabilities.contains_key(*c))
1624 .map(String::as_str)
1625 }
1626 }
1627
1628 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1630 let canonical = self.canonical_id(id)?.to_string();
1631 let removed = self.capabilities.remove(&canonical);
1632 self.aliases.retain(|_, target| *target != canonical);
1633 removed
1634 }
1635
1636 pub fn has(&self, id: &str) -> bool {
1638 self.get(id).is_some()
1639 }
1640
1641 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1643 self.capabilities.values().collect()
1644 }
1645
1646 pub fn len(&self) -> usize {
1648 self.capabilities.len()
1649 }
1650
1651 pub fn is_empty(&self) -> bool {
1653 self.capabilities.is_empty()
1654 }
1655
1656 pub fn builder() -> CapabilityRegistryBuilder {
1658 CapabilityRegistryBuilder::new()
1659 }
1660
1661 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1665 for cap in self.capabilities.values() {
1666 for bp in cap.agent_blueprints() {
1667 if bp.id == id {
1668 return Some(bp);
1669 }
1670 }
1671 }
1672 None
1673 }
1674
1675 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1679 for (capability_id, cap) in &self.capabilities {
1680 for bp in cap.agent_blueprints() {
1681 if bp.id == id {
1682 return Some((capability_id.clone(), bp));
1683 }
1684 }
1685 }
1686 None
1687 }
1688
1689 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1691 self.capabilities
1692 .values()
1693 .flat_map(|cap| cap.agent_blueprints())
1694 .collect()
1695 }
1696}
1697
1698impl Default for CapabilityRegistry {
1699 fn default() -> Self {
1700 Self::with_builtins()
1701 }
1702}
1703
1704impl std::fmt::Debug for CapabilityRegistry {
1705 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1706 let ids: Vec<_> = self.capabilities.keys().collect();
1707 f.debug_struct("CapabilityRegistry")
1708 .field("capabilities", &ids)
1709 .finish()
1710 }
1711}
1712
1713pub struct CapabilityRegistryBuilder {
1715 registry: CapabilityRegistry,
1716}
1717
1718impl CapabilityRegistryBuilder {
1719 pub fn new() -> Self {
1721 Self {
1722 registry: CapabilityRegistry::new(),
1723 }
1724 }
1725
1726 pub fn with_builtins() -> Self {
1728 Self {
1729 registry: CapabilityRegistry::with_builtins(),
1730 }
1731 }
1732
1733 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1735 self.registry.register(capability);
1736 self
1737 }
1738
1739 pub fn build(self) -> CapabilityRegistry {
1741 self.registry
1742 }
1743}
1744
1745impl Default for CapabilityRegistryBuilder {
1746 fn default() -> Self {
1747 Self::new()
1748 }
1749}
1750
1751pub struct ModelViewContext<'a> {
1757 pub session_id: SessionId,
1758 pub prior_usage: Option<&'a TokenUsage>,
1759}
1760
1761pub trait ModelViewProvider: Send + Sync {
1767 fn apply_model_view(
1768 &self,
1769 messages: Vec<Message>,
1770 config: &serde_json::Value,
1771 context: &ModelViewContext<'_>,
1772 ) -> Vec<Message>;
1773
1774 fn priority(&self) -> i32 {
1775 0
1776 }
1777}
1778
1779pub struct CollectedCapabilities {
1784 pub system_prompt_parts: Vec<String>,
1786 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1788 pub tools: Vec<Box<dyn Tool>>,
1790 pub tool_definitions: Vec<ToolDefinition>,
1792 pub mounts: Vec<MountPoint>,
1794 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1796 pub applied_ids: Vec<String>,
1798 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1800 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1802 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1805 pub parallel_tool_calls: Option<bool>,
1809 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1811 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1813 pub mcp_servers: ScopedMcpServers,
1815 }
1821
1822#[derive(Debug, Clone, PartialEq, Eq)]
1823pub struct SystemPromptAttribution {
1824 pub capability_id: String,
1825 pub content: String,
1826}
1827
1828impl CollectedCapabilities {
1829 pub fn system_prompt_prefix(&self) -> Option<String> {
1832 if self.system_prompt_parts.is_empty() {
1833 None
1834 } else {
1835 Some(self.system_prompt_parts.join("\n\n"))
1836 }
1837 }
1838
1839 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1843 for (provider, config) in &self.message_filter_providers {
1845 provider.apply_filters(query, config);
1846 }
1847 }
1848
1849 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1852 for (provider, config) in &self.message_filter_providers {
1853 provider.post_load(messages, config);
1854 }
1855 }
1856
1857 pub fn has_message_filters(&self) -> bool {
1859 !self.message_filter_providers.is_empty()
1860 }
1861}
1862
1863struct SpawnAgentTargetProvider {
1864 target_type: &'static str,
1865 tool: Box<dyn Tool>,
1866}
1867
1868#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1870#[serde(rename_all = "snake_case")]
1871pub(crate) enum SpawnMode {
1872 Background,
1873 Foreground,
1874}
1875
1876impl SpawnMode {
1877 pub(crate) fn parse(value: &str) -> Option<Self> {
1878 match value {
1879 "background" => Some(Self::Background),
1880 "foreground" => Some(Self::Foreground),
1881 _ => None,
1882 }
1883 }
1884
1885 pub(crate) fn as_str(self) -> &'static str {
1886 match self {
1887 Self::Background => "background",
1888 Self::Foreground => "foreground",
1889 }
1890 }
1891}
1892
1893struct UnifiedSpawnAgentTool {
1894 providers: Vec<SpawnAgentTargetProvider>,
1895}
1896
1897impl UnifiedSpawnAgentTool {
1898 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1899 Self { providers }
1900 }
1901
1902 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1903 self.providers
1904 .iter()
1905 .find(|provider| provider.target_type == target_type)
1906 .map(|provider| provider.tool.as_ref())
1907 }
1908
1909 fn target_types(&self) -> Vec<&'static str> {
1910 ["subagent", "agent", "external_a2a"]
1911 .into_iter()
1912 .filter(|target_type| {
1913 self.providers
1914 .iter()
1915 .any(|provider| provider.target_type == *target_type)
1916 })
1917 .collect()
1918 }
1919
1920 fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
1925 self.target_types()
1926 .into_iter()
1927 .filter_map(|target_type| match target_type {
1928 "subagent" => Some(serde_json::json!({
1929 "properties": {
1930 "type": {"const": "subagent"}
1931 }
1932 })),
1933 "agent" => Some(serde_json::json!({
1934 "properties": {
1935 "type": {"const": "agent"}
1936 },
1937 "required": ["type", "id"]
1938 })),
1939 "external_a2a" => Some(serde_json::json!({
1940 "properties": {
1941 "type": {"const": "external_a2a"}
1942 },
1943 "anyOf": [
1944 {"required": ["id"]},
1945 {"required": ["external_agent_id"]}
1946 ]
1947 })),
1948 _ => None,
1949 })
1950 .collect()
1951 }
1952
1953 }
1963
1964#[async_trait]
1965impl Tool for UnifiedSpawnAgentTool {
1966 fn narrate(
1967 &self,
1968 tool_call: &ToolCall,
1969 phase: crate::tool_narration::ToolNarrationPhase,
1970 locale: Option<&str>,
1971 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1972 ) -> Option<String> {
1973 let target_type = tool_call
1974 .arguments
1975 .get("target")
1976 .and_then(|target| target.get("type"))
1977 .and_then(serde_json::Value::as_str)?;
1978 self.provider_for(target_type)
1979 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1980 }
1981
1982 fn name(&self) -> &str {
1983 "spawn_agent"
1984 }
1985
1986 fn display_name(&self) -> Option<&str> {
1987 Some("Spawn Agent")
1988 }
1989
1990 fn description(&self) -> &str {
1991 "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."
1992 }
1993
1994 fn parameters_schema(&self) -> serde_json::Value {
1995 serde_json::json!({
1996 "type": "object",
1997 "properties": {
1998 "name": {
1999 "type": "string",
2000 "description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
2001 },
2002 "instructions": {
2003 "type": "string",
2004 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
2005 },
2006 "goal": {
2007 "type": "string",
2008 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
2009 },
2010 "lifetime": {
2011 "type": "string",
2012 "enum": ["linked", "detached"],
2013 "default": "linked",
2014 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
2015 },
2016 "seed": {
2017 "type": "string",
2018 "enum": ["fresh", "fork", "workspace"],
2019 "default": "fresh",
2020 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
2021 },
2022 "target": {
2023 "type": "object",
2024 "properties": {
2025 "type": {
2026 "type": "string",
2027 "enum": self.target_types(),
2028 "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."
2029 },
2030 "id": {
2031 "type": "string",
2032 "description": "Configured target id for first-party handoffs or external A2A agents."
2033 },
2034 "external_agent_id": {
2035 "type": "string",
2036 "description": "Configured external A2A agent id."
2037 }
2038 },
2039 "required": ["type"],
2040 "oneOf": self.target_constraint_branches(),
2041 "additionalProperties": false
2042 },
2043 "mode": {
2044 "type": "string",
2045 "enum": ["background", "foreground"],
2046 "description": "Execution mode. Use background to return immediately with a task_id, or foreground to block until the delegated work reaches a terminal state or timeout."
2047 },
2048 "blueprint": {
2049 "type": "string",
2050 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
2051 },
2052 "config": {
2053 "type": "object",
2054 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
2055 },
2056 "result_schema": {
2057 "type": "object",
2058 "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."
2059 },
2060 "message_schema": {
2061 "type": "object",
2062 "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."
2063 },
2064 "public_context": {
2065 "type": "object",
2066 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
2067 },
2068 "wait_timeout_secs": {
2069 "type": "integer",
2070 "minimum": 1,
2071 "maximum": 86400,
2072 "description": "External-A2A-only foreground timeout."
2073 },
2074 "wake_on_completion": {
2075 "type": "boolean",
2076 "description": "External-A2A-only control for background completion wake-ups."
2077 }
2078 },
2079 "required": ["name", "instructions", "target"],
2080 "additionalProperties": false
2081 })
2082 }
2083
2084 fn hints(&self) -> crate::tool_types::ToolHints {
2085 let mut hints = crate::tool_types::ToolHints::default()
2086 .with_long_running(true)
2087 .with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
2088 if self.provider_for("external_a2a").is_some() {
2089 hints = hints.with_open_world(true);
2090 }
2091 hints
2092 }
2093
2094 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
2095 ToolExecutionResult::tool_error(
2096 "spawn_agent requires context. This tool must be executed with session context.",
2097 )
2098 }
2099
2100 async fn execute_with_context(
2101 &self,
2102 arguments: serde_json::Value,
2103 context: &ToolContext,
2104 ) -> ToolExecutionResult {
2105 let target_type = match arguments
2106 .get("target")
2107 .and_then(|target| target.get("type"))
2108 .and_then(serde_json::Value::as_str)
2109 {
2110 Some(target_type) => target_type,
2111 None => {
2112 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
2113 }
2114 };
2115
2116 let Some(provider) = self.provider_for(target_type) else {
2117 let supported = self.target_types().join(", ");
2118 return ToolExecutionResult::tool_error(format!(
2119 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
2120 ));
2121 };
2122 if target_type == "external_a2a"
2123 && arguments
2124 .get("lifetime")
2125 .and_then(serde_json::Value::as_str)
2126 .is_some_and(|value| value == "detached")
2127 {
2128 return ToolExecutionResult::tool_error(
2129 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2130 );
2131 }
2132 if target_type == "external_a2a"
2133 && arguments
2134 .get("message_schema")
2135 .is_some_and(|schema| !schema.is_null())
2136 {
2137 return ToolExecutionResult::tool_error(
2138 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2139 );
2140 }
2141
2142 provider.execute_with_context(arguments, context).await
2143 }
2144
2145 fn requires_context(&self) -> bool {
2146 true
2147 }
2148}
2149
2150pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2155 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2156 return base_system_prompt.to_string();
2157 };
2158
2159 if base_system_prompt.is_empty() {
2160 return additions.to_string();
2161 }
2162
2163 if base_system_prompt.contains("<system-prompt>") {
2164 format!("{base_system_prompt}\n\n{additions}")
2165 } else {
2166 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2167 }
2168}
2169
2170pub struct CollectedMessageFilters {
2177 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2179}
2180
2181pub struct CollectedModelViewProviders {
2183 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2185}
2186
2187impl CollectedMessageFilters {
2193 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2195 for (provider, config) in &self.message_filter_providers {
2196 provider.apply_filters(query, config);
2197 }
2198 }
2199
2200 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2202 for (provider, config) in &self.message_filter_providers {
2203 provider.post_load(messages, config);
2204 }
2205 }
2206}
2207
2208impl CollectedModelViewProviders {
2209 pub fn apply_model_view(
2211 &self,
2212 mut messages: Vec<Message>,
2213 context: &ModelViewContext<'_>,
2214 ) -> Vec<Message> {
2215 for (provider, config) in &self.model_view_providers {
2216 messages = provider.apply_model_view(messages, config, context);
2217 }
2218 messages
2219 }
2220}
2221
2222fn compaction_is_enabled(
2228 capability_configs: &[AgentCapabilityConfig],
2229 registry: &CapabilityRegistry,
2230) -> bool {
2231 capability_configs.iter().any(|cap_config| {
2232 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2233 && registry
2234 .get(cap_config.capability_ref.as_str())
2235 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2236 })
2237}
2238
2239fn message_filter_config_for(
2248 cap_id: &str,
2249 base: &serde_json::Value,
2250 compaction_on: bool,
2251) -> serde_json::Value {
2252 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2253 return base.clone();
2254 }
2255 let mut config = base.clone();
2256 match config.as_object_mut() {
2257 Some(map) => {
2258 map.insert(
2259 "compaction_active".to_string(),
2260 serde_json::Value::Bool(true),
2261 );
2262 }
2263 None => {
2264 config = serde_json::json!({ "compaction_active": true });
2265 }
2266 }
2267 config
2268}
2269
2270pub fn collect_message_filters_only(
2276 capability_configs: &[AgentCapabilityConfig],
2277 registry: &CapabilityRegistry,
2278) -> CollectedMessageFilters {
2279 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2280 Vec::new();
2281 let compaction_on = compaction_is_enabled(capability_configs, registry);
2282
2283 for cap_config in capability_configs {
2284 let cap_id = cap_config.capability_ref.as_str();
2285 if let Some(capability) = registry.get(cap_id) {
2286 if capability.status() != CapabilityStatus::Available {
2287 continue;
2288 }
2289 let effective: &dyn Capability = capability
2292 .resolve_for_model(None)
2293 .unwrap_or_else(|| capability.as_ref());
2294 if let Some(provider) = effective.message_filter_provider() {
2295 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2296 message_filter_providers.push((provider, config));
2297 }
2298 }
2299 }
2300
2301 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2302
2303 CollectedMessageFilters {
2304 message_filter_providers,
2305 }
2306}
2307
2308pub fn collect_model_view_providers(
2315 capability_configs: &[AgentCapabilityConfig],
2316 registry: &CapabilityRegistry,
2317 model: Option<&str>,
2318) -> CollectedModelViewProviders {
2319 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2320
2321 for cap_config in capability_configs {
2322 let cap_id = cap_config.capability_ref.as_str();
2323 if let Some(capability) = registry.get(cap_id) {
2324 if capability.status() != CapabilityStatus::Available {
2325 continue;
2326 }
2327 let effective: &dyn Capability = capability
2328 .resolve_for_model(model)
2329 .unwrap_or_else(|| capability.as_ref());
2330 if let Some(provider) = effective.model_view_provider() {
2331 model_view_providers.push((provider, cap_config.config.clone()));
2332 }
2333 }
2334 }
2335
2336 model_view_providers.sort_by_key(|(p, _)| p.priority());
2337
2338 CollectedModelViewProviders {
2339 model_view_providers,
2340 }
2341}
2342
2343pub fn collect_dynamic_facts(
2349 capability_configs: &[AgentCapabilityConfig],
2350 registry: &CapabilityRegistry,
2351 model: Option<&str>,
2352 ctx: &FactsContext,
2353) -> Vec<Fact> {
2354 let mut dynamic = Vec::new();
2355 for cap_config in capability_configs {
2356 let cap_id = cap_config.capability_ref.as_str();
2357 if let Some(capability) = registry.get(cap_id) {
2358 if capability.status() != CapabilityStatus::Available {
2359 continue;
2360 }
2361 let effective: &dyn Capability = capability
2362 .resolve_for_model(model)
2363 .unwrap_or_else(|| capability.as_ref());
2364 for fact in effective.facts(&cap_config.config, ctx) {
2365 if fact.volatility == Volatility::Dynamic {
2366 dynamic.push(fact);
2367 }
2368 }
2369 }
2370 }
2371 dynamic
2372}
2373
2374pub fn collect_capability_mcp_servers(
2375 capability_configs: &[AgentCapabilityConfig],
2376 registry: &CapabilityRegistry,
2377) -> ScopedMcpServers {
2378 let mut servers = ScopedMcpServers::default();
2379
2380 for cap_config in capability_configs {
2381 let cap_id = cap_config.capability_ref.as_str();
2382 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2385 if let Ok(definition) =
2386 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2387 {
2388 if definition.status != CapabilityStatus::Available {
2389 continue;
2390 }
2391 if let Some(contributed) = definition.mcp_servers {
2392 servers = merge_scoped_mcp_servers(&servers, &contributed);
2393 }
2394 }
2395 continue;
2396 }
2397 if let Some(capability) = registry.get(cap_id) {
2398 if capability.status() != CapabilityStatus::Available {
2399 continue;
2400 }
2401 servers = merge_scoped_mcp_servers(
2402 &servers,
2403 &capability.mcp_servers_with_config(&cap_config.config),
2404 );
2405 }
2406 }
2407
2408 servers
2409}
2410
2411pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2418
2419#[derive(Debug, Clone, PartialEq, Eq)]
2421pub enum DependencyError {
2422 CircularDependency {
2424 capability_id: String,
2426 chain: Vec<String>,
2428 },
2429 TooManyCapabilities {
2431 count: usize,
2433 max: usize,
2435 },
2436}
2437
2438impl std::fmt::Display for DependencyError {
2439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2440 match self {
2441 DependencyError::CircularDependency {
2442 capability_id,
2443 chain,
2444 } => {
2445 write!(
2446 f,
2447 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2448 capability_id,
2449 chain.join(" -> "),
2450 capability_id
2451 )
2452 }
2453 DependencyError::TooManyCapabilities { count, max } => {
2454 write!(
2455 f,
2456 "Too many capabilities after resolution: {} (max: {})",
2457 count, max
2458 )
2459 }
2460 }
2461 }
2462}
2463
2464impl std::error::Error for DependencyError {}
2465
2466#[derive(Debug, Clone)]
2468pub struct ResolvedCapabilities {
2469 pub resolved_ids: Vec<String>,
2472 pub added_as_dependencies: Vec<String>,
2474 pub user_selected: Vec<String>,
2476}
2477
2478pub fn resolve_dependencies(
2498 selected_ids: &[String],
2499 registry: &CapabilityRegistry,
2500) -> Result<ResolvedCapabilities, DependencyError> {
2501 use std::collections::HashSet;
2502
2503 let user_selected: HashSet<String> = selected_ids
2505 .iter()
2506 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2507 .collect();
2508 let mut resolved: Vec<String> = Vec::new();
2509 let mut resolved_set: HashSet<String> = HashSet::new();
2510 let mut added_as_dependencies: Vec<String> = Vec::new();
2511
2512 for cap_id in selected_ids {
2514 resolve_single_capability(
2515 cap_id,
2516 registry,
2517 &mut resolved,
2518 &mut resolved_set,
2519 &mut added_as_dependencies,
2520 &user_selected,
2521 &mut Vec::new(), )?;
2523 }
2524
2525 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2527 return Err(DependencyError::TooManyCapabilities {
2528 count: resolved.len(),
2529 max: MAX_RESOLVED_CAPABILITIES,
2530 });
2531 }
2532
2533 Ok(ResolvedCapabilities {
2534 resolved_ids: resolved,
2535 added_as_dependencies,
2536 user_selected: selected_ids.to_vec(),
2537 })
2538}
2539
2540pub fn resolve_capability_configs(
2545 selected_configs: &[AgentCapabilityConfig],
2546 registry: &CapabilityRegistry,
2547) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2548 let mut selected_ids: Vec<String> = Vec::new();
2549 for config in selected_configs {
2550 if (is_declarative_capability(config.capability_id())
2553 || is_plugin_capability(config.capability_id()))
2554 && let Ok(definition) =
2555 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2556 {
2557 selected_ids.extend(definition.dependencies);
2558 }
2559 selected_ids.push(config.capability_id().to_string());
2560 }
2561 let resolved = resolve_dependencies(&selected_ids, registry)?;
2562
2563 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2566 .iter()
2567 .map(|config| {
2568 let id = config.capability_id();
2569 let id = registry.canonical_id(id).unwrap_or(id);
2570 (id.to_string(), config.config.clone())
2571 })
2572 .collect();
2573
2574 Ok(resolved
2575 .resolved_ids
2576 .into_iter()
2577 .map(|capability_id| {
2578 explicit_configs
2579 .get(&capability_id)
2580 .cloned()
2581 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2582 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2583 })
2584 .collect())
2585}
2586
2587fn resolve_single_capability(
2589 cap_id: &str,
2590 registry: &CapabilityRegistry,
2591 resolved: &mut Vec<String>,
2592 resolved_set: &mut std::collections::HashSet<String>,
2593 added_as_dependencies: &mut Vec<String>,
2594 user_selected: &std::collections::HashSet<String>,
2595 visiting: &mut Vec<String>,
2596) -> Result<(), DependencyError> {
2597 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2601
2602 if resolved_set.contains(cap_id) {
2604 return Ok(());
2605 }
2606
2607 if visiting.contains(&cap_id.to_string()) {
2609 return Err(DependencyError::CircularDependency {
2610 capability_id: cap_id.to_string(),
2611 chain: visiting.clone(),
2612 });
2613 }
2614
2615 let capability = match registry.get(cap_id) {
2617 Some(cap) => cap,
2618 None => {
2619 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2623 && !resolved_set.contains(cap_id)
2624 {
2625 resolved.push(cap_id.to_string());
2626 resolved_set.insert(cap_id.to_string());
2627 if !user_selected.contains(cap_id) {
2628 added_as_dependencies.push(cap_id.to_string());
2629 }
2630 }
2631 return Ok(());
2632 }
2633 };
2634
2635 visiting.push(cap_id.to_string());
2637
2638 for dep_id in capability.dependencies() {
2640 resolve_single_capability(
2641 dep_id,
2642 registry,
2643 resolved,
2644 resolved_set,
2645 added_as_dependencies,
2646 user_selected,
2647 visiting,
2648 )?;
2649 }
2650
2651 visiting.pop();
2653
2654 if !resolved_set.contains(cap_id) {
2656 resolved.push(cap_id.to_string());
2657 resolved_set.insert(cap_id.to_string());
2658
2659 if !user_selected.contains(cap_id) {
2661 added_as_dependencies.push(cap_id.to_string());
2662 }
2663 }
2664
2665 Ok(())
2666}
2667
2668pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2673 use std::collections::HashSet;
2674
2675 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2676 Ok(resolved) => resolved.resolved_ids,
2677 Err(_) => capability_ids.to_vec(),
2678 };
2679
2680 let mut seen = HashSet::new();
2681 let mut features = Vec::new();
2682 for cap_id in &resolved_ids {
2683 if let Some(cap) = registry.get(cap_id) {
2684 for feature in cap.features() {
2685 if seen.insert(feature) {
2686 features.push(feature.to_string());
2687 }
2688 }
2689 }
2690 }
2691 features
2692}
2693
2694pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2697 registry
2698 .get(cap_id)
2699 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2700 .unwrap_or_default()
2701}
2702
2703pub async fn collect_capabilities(
2719 capability_ids: &[String],
2720 registry: &CapabilityRegistry,
2721 ctx: &SystemPromptContext,
2722) -> CollectedCapabilities {
2723 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2726 Ok(resolved) => resolved.resolved_ids,
2727 Err(e) => {
2728 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2729 capability_ids.to_vec()
2730 }
2731 };
2732
2733 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2735 .iter()
2736 .map(|id| AgentCapabilityConfig {
2737 capability_ref: CapabilityId::new(id),
2738 config: serde_json::Value::Object(serde_json::Map::new()),
2739 })
2740 .collect();
2741
2742 collect_capabilities_with_configs(&configs, registry, ctx).await
2743}
2744
2745pub async fn collect_capabilities_with_configs(
2756 capability_configs: &[AgentCapabilityConfig],
2757 registry: &CapabilityRegistry,
2758 ctx: &SystemPromptContext,
2759) -> CollectedCapabilities {
2760 let mut system_prompt_parts: Vec<String> = Vec::new();
2761 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2762 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2763 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2764 let mut mounts: Vec<MountPoint> = Vec::new();
2765 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2766 Vec::new();
2767 let mut applied_ids: Vec<String> = Vec::new();
2768 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2769 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2770 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2771 let mut parallel_tool_calls: Option<bool> = None;
2772 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2773 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2774 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2777 let mut mcp_servers = ScopedMcpServers::default();
2778 let mut static_facts: Vec<Fact> = Vec::new();
2782 let mut has_dynamic_facts = false;
2783 let facts_ctx = FactsContext::new(ctx.session_id);
2784 let compaction_on = compaction_is_enabled(capability_configs, registry);
2785 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2786 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2787
2788 for cap_config in capability_configs {
2789 let cap_id = cap_config.capability_ref.as_str();
2790 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2795 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2796 cap_config.config.clone(),
2797 ) {
2798 Ok(definition) => {
2799 if definition.status != CapabilityStatus::Available {
2800 continue;
2801 }
2802
2803 if let Some(prompt) = definition.system_prompt.as_deref() {
2804 let contribution =
2805 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2806 system_prompt_attributions.push(SystemPromptAttribution {
2807 capability_id: cap_id.to_string(),
2808 content: contribution.clone(),
2809 });
2810 system_prompt_parts.push(contribution);
2811 }
2812
2813 mounts.extend(definition.mounts(cap_id));
2814 if let Some(ref servers) = definition.mcp_servers {
2815 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2816 }
2817 for skill in definition.skill_contributions() {
2818 mounts.push(skill.to_mount(cap_id));
2819 }
2820
2821 applied_ids.push(cap_id.to_string());
2822 }
2823 Err(error) => {
2824 tracing::warn!(
2825 capability_id = %cap_id,
2826 error = %error,
2827 "Skipping invalid declarative/plugin capability config"
2828 );
2829 }
2830 }
2831 continue;
2832 }
2833 if let Some(capability) = registry.get(cap_id) {
2834 if capability.status() != CapabilityStatus::Available {
2836 continue;
2837 }
2838
2839 let effective: &dyn Capability =
2851 match capability.resolve_for_model(ctx.model.as_deref()) {
2852 Some(inner) => inner,
2853 None => capability.as_ref(),
2854 };
2855 let effective_id = effective.id();
2856 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2857 agent_handoff_spawn_config = Some(cap_config.config.clone());
2858 }
2859
2860 if let Some(contribution) = effective
2862 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2863 .await
2864 {
2865 system_prompt_attributions.push(SystemPromptAttribution {
2866 capability_id: cap_id.to_string(),
2867 content: contribution.clone(),
2868 });
2869 system_prompt_parts.push(contribution);
2870 }
2871
2872 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2877 match fact.volatility {
2878 Volatility::Static => static_facts.push(fact),
2879 Volatility::Dynamic => has_dynamic_facts = true,
2880 }
2881 }
2882
2883 for tool in effective.tools_with_config(&cap_config.config) {
2885 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2886 spawn_agent_providers.push(SpawnAgentTargetProvider {
2887 target_type: "external_a2a",
2888 tool,
2889 });
2890 } else {
2891 tools.push(tool);
2892 }
2893 }
2894 tool_definition_hooks
2895 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2896 tool_call_hooks.extend(effective.tool_call_hooks());
2897 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2899 let cap_category = effective.category();
2904 for def in effective.tool_definitions() {
2905 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2906 continue;
2907 }
2908 let def = match (def.category(), cap_category) {
2909 (None, Some(cat)) => def.with_category(cat),
2910 _ => def,
2911 }
2912 .with_capability_attribution(cap_id, Some(capability.name()));
2913 tool_definitions.push(def);
2914 }
2915
2916 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2924 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2925 {
2926 let threshold = cap_config
2928 .config
2929 .get("threshold")
2930 .and_then(|v| v.as_u64())
2931 .map(|v| v as usize)
2932 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2933 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2934 enabled: true,
2935 threshold,
2936 });
2937 }
2938
2939 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2940 let strategy = cap_config
2941 .config
2942 .get("strategy")
2943 .and_then(|v| v.as_str())
2944 .map(|value| match value {
2945 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2946 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2947 })
2948 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2949 let gemini_cached_content = cap_config
2950 .config
2951 .get("gemini_cached_content")
2952 .and_then(|v| v.as_str())
2953 .map(str::to_string);
2954 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2955 enabled: true,
2956 strategy,
2957 gemini_cached_content,
2958 });
2959 }
2960
2961 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2962 parallel_tool_calls =
2963 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2964 }
2965
2966 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2967 let server_tools =
2968 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2969 if !server_tools.is_empty() {
2970 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2971 server_tools,
2972 ..Default::default()
2973 });
2974 }
2975 }
2976
2977 mounts.extend(effective.mounts());
2979
2980 mcp_servers = merge_scoped_mcp_servers(
2981 &mcp_servers,
2982 &effective.mcp_servers_with_config(&cap_config.config),
2983 );
2984
2985 for skill in effective.contribute_skills() {
2989 mounts.push(skill.to_mount(cap_id));
2990 }
2991
2992 if let Some(provider) = effective.message_filter_provider() {
2994 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2995 message_filter_providers.push((provider, config));
2996 }
2997
2998 applied_ids.push(cap_id.to_string());
2999 }
3000 }
3001
3002 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
3007 spawn_agent_providers.push(SpawnAgentTargetProvider {
3008 target_type: "subagent",
3009 tool: Box::new(SpawnSubagentAsAgentTool),
3010 });
3011 }
3012 if let Some(config) = agent_handoff_spawn_config.as_ref() {
3013 spawn_agent_providers.push(SpawnAgentTargetProvider {
3014 target_type: "agent",
3015 tool: Box::new(SpawnAgentHandoffTool::new(config)),
3016 });
3017 }
3018 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
3019 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
3020 let def = tool
3021 .to_definition()
3022 .with_category("Orchestration")
3023 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
3024 tools.push(Box::new(tool));
3025 tool_definitions.push(def);
3026 }
3027
3028 if !applied_ids
3040 .iter()
3041 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
3042 && tool_definitions
3043 .iter()
3044 .any(|def| def.hints().supports_background == Some(true))
3045 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
3046 && bg_cap.status() == CapabilityStatus::Available
3047 {
3048 tools.extend(bg_cap.tools());
3049 let cap_category = bg_cap.category();
3050 for def in bg_cap.tool_definitions() {
3051 let def = match (def.category(), cap_category) {
3052 (None, Some(cat)) => def.with_category(cat),
3053 _ => def,
3054 }
3055 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
3056 tool_definitions.push(def);
3057 }
3058 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
3059 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
3060 }
3061
3062 if let Some(block) = facts::render_facts_block(&static_facts) {
3067 system_prompt_attributions.push(SystemPromptAttribution {
3068 capability_id: "facts".to_string(),
3069 content: block.clone(),
3070 });
3071 system_prompt_parts.push(block);
3072 }
3073 if has_dynamic_facts {
3074 system_prompt_attributions.push(SystemPromptAttribution {
3075 capability_id: "facts".to_string(),
3076 content: FACTS_DYNAMIC_NOTE.to_string(),
3077 });
3078 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
3079 }
3080
3081 tool_call_hooks.extend(narration_hooks);
3085
3086 message_filter_providers.sort_by_key(|(p, _)| p.priority());
3088
3089 CollectedCapabilities {
3090 system_prompt_parts,
3091 system_prompt_attributions,
3092 tools,
3093 tool_definitions,
3094 mounts,
3095 message_filter_providers,
3096 applied_ids,
3097 tool_search,
3098 prompt_cache,
3099 openrouter_routing,
3100 parallel_tool_calls,
3101 tool_definition_hooks,
3102 tool_call_hooks,
3103 mcp_servers,
3104 }
3105}
3106
3107pub struct AppliedCapabilities {
3113 pub runtime_agent: RuntimeAgent,
3115 pub tool_registry: ToolRegistry,
3117 pub applied_ids: Vec<String>,
3119}
3120
3121pub async fn apply_capabilities(
3158 base_runtime_agent: RuntimeAgent,
3159 capability_ids: &[String],
3160 registry: &CapabilityRegistry,
3161 ctx: &SystemPromptContext,
3162) -> AppliedCapabilities {
3163 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3164
3165 let final_system_prompt = compose_system_prompt(
3167 &base_runtime_agent.system_prompt,
3168 collected.system_prompt_prefix().as_deref(),
3169 );
3170
3171 let mut tool_registry = ToolRegistry::new();
3173 for tool in collected.tools {
3174 tool_registry.register_boxed(tool);
3175 }
3176
3177 let mut tools = collected.tool_definitions;
3179 for hook in &collected.tool_definition_hooks {
3180 tools = hook.transform(tools);
3181 }
3182
3183 let runtime_agent = RuntimeAgent {
3184 system_prompt: final_system_prompt,
3185 model: base_runtime_agent.model,
3186 tools,
3187 max_iterations: base_runtime_agent.max_iterations,
3188 temperature: base_runtime_agent.temperature,
3189 max_tokens: base_runtime_agent.max_tokens,
3190 tool_search: collected.tool_search,
3191 prompt_cache: collected.prompt_cache,
3192 openrouter_routing: collected.openrouter_routing,
3193 network_access: base_runtime_agent.network_access,
3194 parallel_tool_calls: base_runtime_agent
3197 .parallel_tool_calls
3198 .or(collected.parallel_tool_calls),
3199 };
3200
3201 AppliedCapabilities {
3202 runtime_agent,
3203 tool_registry,
3204 applied_ids: collected.applied_ids,
3205 }
3206}
3207
3208#[cfg(test)]
3213mod tests {
3214 use super::*;
3215 use crate::typed_id::SessionId;
3216 use std::collections::BTreeSet;
3217 use uuid::Uuid;
3218
3219 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3221
3222 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3223 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3224 }
3225
3226 fn test_ctx() -> SystemPromptContext {
3228 SystemPromptContext::without_file_store(SessionId::new())
3229 }
3230
3231 struct HostAnnotatedCapability;
3233
3234 #[async_trait]
3235 impl Capability for HostAnnotatedCapability {
3236 fn id(&self) -> &str {
3237 "host_annotated"
3238 }
3239 fn name(&self) -> &str {
3240 "Host Annotated"
3241 }
3242 fn description(&self) -> &str {
3243 "Test capability with host-owned metadata."
3244 }
3245 fn metadata(&self) -> Option<serde_json::Value> {
3246 Some(serde_json::json!({"icon": "sparkles", "group": "host"}))
3247 }
3248 }
3249
3250 #[test]
3251 fn capability_metadata_is_an_opt_in_host_hatch() {
3252 assert!(NoopCapability.metadata().is_none());
3254
3255 let metadata = HostAnnotatedCapability.metadata().expect("metadata");
3256 assert_eq!(metadata["icon"], "sparkles");
3257 assert_eq!(metadata["group"], "host");
3258 }
3259
3260 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3262 let mut ids = [
3263 "agent_instructions",
3264 "human_intent",
3265 "budgeting",
3266 "self_budget",
3267 "noop",
3268 "current_time",
3269 "research",
3270 "session_file_system",
3271 "session_storage",
3272 "session",
3273 "session_sql_database",
3274 "test_math",
3275 "test_weather",
3276 "stateless_todo_list",
3277 "web_fetch",
3278 "bashkit_shell",
3279 "background_execution",
3280 "session_schedule",
3281 "btw",
3282 "infinity_context",
3283 "compaction",
3284 "memory",
3285 "message_metadata",
3286 "openai_tool_search",
3287 "claude_tool_search",
3288 "tool_search",
3289 "auto_tool_search",
3290 "prompt_caching",
3291 "parallel_tool_calls",
3292 "session_tasks",
3293 "skills",
3294 "subagents",
3295 "system_commands",
3296 "sample_data",
3297 "data_knowledge",
3298 "knowledge_base",
3299 "knowledge_index",
3300 "citation_retrieval",
3301 "citation_verification",
3302 "tool_output_persistence",
3303 "tool_output_distillation",
3304 "fake_warehouse",
3305 "fake_aws",
3306 "fake_crm",
3307 "fake_financial",
3308 "loop_detection",
3309 "progress_guard",
3310 "usage_limit_auto_continue",
3311 "tool_call_repair",
3312 "error_disclosure",
3313 "prompt_canary_guardrail",
3314 "guardrails",
3315 "user_hooks",
3316 "model_scout",
3317 "openrouter_workspace",
3318 "openrouter_server_tools",
3319 ]
3320 .into_iter()
3321 .collect::<BTreeSet<_>>();
3322 if cfg!(feature = "ui-capabilities") {
3323 ids.insert("openui");
3324 ids.insert("a2ui");
3325 }
3326 ids
3327 }
3328
3329 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3331 let mut ids = [
3332 "agent_instructions",
3333 "human_intent",
3334 "budgeting",
3335 "self_budget",
3336 "noop",
3337 "current_time",
3338 "session_file_system",
3339 "session_storage",
3340 "session",
3341 "stateless_todo_list",
3342 "bashkit_shell",
3343 "btw",
3344 "infinity_context",
3345 "compaction",
3346 "message_metadata",
3347 "openai_tool_search",
3348 "claude_tool_search",
3349 "tool_search",
3350 "auto_tool_search",
3351 "prompt_caching",
3352 "parallel_tool_calls",
3353 "skills",
3354 "system_commands",
3355 "tool_output_persistence",
3356 "tool_output_distillation",
3357 "loop_detection",
3358 "progress_guard",
3359 "tool_call_repair",
3360 "error_disclosure",
3361 "prompt_canary_guardrail",
3362 "guardrails",
3363 "user_hooks",
3364 ]
3365 .into_iter()
3366 .collect::<BTreeSet<_>>();
3367 if cfg!(feature = "web-fetch") {
3368 ids.insert("web_fetch");
3369 }
3370 ids
3371 }
3372
3373 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3375 let mut ids = expected_core_builtin_ids();
3376 ids.insert("agent_handoff");
3377 ids.insert("a2a_agent_delegation");
3378 ids
3379 }
3380
3381 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3382 registry.capabilities.keys().map(String::as_str).collect()
3383 }
3384
3385 #[test]
3395 fn test_capability_registry_with_builtins_dev() {
3396 let _lock = lock_env();
3398 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3399 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3400 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3401 assert!(registry.has("agent_handoff"));
3402 assert!(registry.has("a2a_agent_delegation"));
3403 }
3404
3405 #[test]
3406 fn test_capability_registry_with_builtins_prod() {
3407 let _lock = lock_env();
3409 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3410 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3411 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3412 assert!(!registry.has("docker_container"));
3414 assert!(!registry.has("agent_handoff"));
3415 assert!(!registry.has("a2a_agent_delegation"));
3416 }
3417
3418 #[test]
3419 fn test_capability_registry_runtime_builtins() {
3420 let _lock = lock_env();
3421 unsafe { std::env::remove_var("FEATURE_LUA") };
3422 let registry = CapabilityRegistry::runtime_builtins();
3423 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3424 assert!(registry.has("session_file_system"));
3425 #[cfg(feature = "web-fetch")]
3426 assert!(registry.has("web_fetch"));
3427 assert!(registry.has("bashkit_shell"));
3428
3429 for platform_only in [
3430 "model_scout",
3431 "openrouter_workspace",
3432 "openrouter_server_tools",
3433 "session_tasks",
3434 "session_schedule",
3435 "subagents",
3436 "background_execution",
3437 "session_sql_database",
3438 "knowledge_base",
3439 "knowledge_index",
3440 "sample_data",
3441 "data_knowledge",
3442 "fake_aws",
3443 "fake_crm",
3444 "fake_financial",
3445 "fake_warehouse",
3446 "test_math",
3447 "test_weather",
3448 "research",
3449 ] {
3450 assert!(
3451 !registry.has(platform_only),
3452 "`{platform_only}` should not be in the runtime default registry"
3453 );
3454 }
3455 }
3456
3457 #[test]
3458 fn test_agent_delegation_enabled_by_env_in_prod() {
3459 let _lock = lock_env();
3461 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3462 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3463 assert!(registry.has("agent_handoff"));
3464 assert!(registry.has("a2a_agent_delegation"));
3465 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3466 }
3467
3468 #[test]
3469 fn test_agent_delegation_disabled_by_env_in_dev() {
3470 let _lock = lock_env();
3472 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3473 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3474 assert!(!registry.has("agent_handoff"));
3475 assert!(!registry.has("a2a_agent_delegation"));
3476 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3477 }
3478
3479 #[test]
3480 fn test_capability_registry_get() {
3481 let registry = CapabilityRegistry::with_builtins();
3482
3483 let noop = registry.get("noop").unwrap();
3484 assert_eq!(noop.id(), "noop");
3485 assert_eq!(noop.name(), "No-Op");
3486 assert_eq!(noop.status(), CapabilityStatus::Available);
3487 }
3488
3489 #[test]
3497 fn builtin_capabilities_satisfy_registry_invariants() {
3498 let registry = CapabilityRegistry::with_builtins();
3499
3500 for cap in registry.list() {
3501 let id = cap.id();
3502 assert!(!id.is_empty(), "capability has an empty id");
3503 assert!(
3504 !cap.name().trim().is_empty(),
3505 "capability `{id}` has an empty name"
3506 );
3507
3508 assert!(
3511 registry.get(id).is_some(),
3512 "capability `{id}` does not resolve by its own id"
3513 );
3514
3515 for dep in cap.dependencies() {
3519 assert!(
3520 registry.get(dep).is_some(),
3521 "capability `{id}` depends on `{dep}`, which is not registered"
3522 );
3523 }
3524
3525 let mut seen = std::collections::HashSet::new();
3528 for tool in cap.tools() {
3529 let name = tool.name().to_string();
3530 assert!(
3531 !name.is_empty(),
3532 "capability `{id}` exposes a tool with an empty name"
3533 );
3534 assert!(
3535 seen.insert(name.clone()),
3536 "capability `{id}` exposes duplicate tool name `{name}`"
3537 );
3538 }
3539
3540 let mut def_seen = std::collections::HashSet::new();
3543 for def in cap.tool_definitions() {
3544 let name = def.name().to_string();
3545 assert!(
3546 !name.is_empty(),
3547 "capability `{id}` advertises a tool definition with an empty name"
3548 );
3549 assert!(
3550 def_seen.insert(name.clone()),
3551 "capability `{id}` advertises duplicate tool definition name `{name}`"
3552 );
3553 }
3554 }
3555 }
3556
3557 #[test]
3569 fn builtin_tools_have_narration_or_documented_generic_fallback() {
3570 use crate::tool_narration::{ToolNarrationContext, ToolNarrationPhase};
3571 use crate::tool_types::ToolCall;
3572
3573 const GENERIC_NARRATION_ALLOWLIST: &[(&str, &str)] = &[
3576 ("sample_data", "demo capability with fixture mounts"),
3578 (
3579 "data_knowledge",
3580 "demo knowledge scaffold; fixture data only",
3581 ),
3582 ("fake_aws", "demo/eval fixture tools"),
3583 ("fake_crm", "demo/eval fixture tools"),
3584 ("fake_financial", "demo/eval fixture tools"),
3585 ("fake_warehouse", "demo/eval fixture tools"),
3586 ("test_math", "test fixture capability"),
3587 ("test_weather", "test fixture capability"),
3588 (
3593 "platform",
3594 "operator command surface; tool display names are the intended presentation",
3595 ),
3596 (
3597 "platform_management",
3598 "operator admin surface; mutations narrate via narration_noun, reads use display names",
3599 ),
3600 (
3603 "model_scout",
3604 "operator model-routing tools; display-name presentation is adequate",
3605 ),
3606 (
3607 "openrouter_workspace",
3608 "operator OpenRouter inspection tools; display-name presentation is adequate",
3609 ),
3610 (
3613 "lua",
3614 "arbitrary sandboxed code execution; display-name presentation is adequate",
3615 ),
3616 ];
3617
3618 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3621 let ctx = ToolNarrationContext::default();
3622 let mut missing: Vec<String> = Vec::new();
3623
3624 for cap in registry.list() {
3625 let cap_id = cap.id().to_string();
3626 if GENERIC_NARRATION_ALLOWLIST
3627 .iter()
3628 .any(|(id, _)| *id == cap_id)
3629 {
3630 continue;
3631 }
3632
3633 for tool in cap.tools() {
3634 let def = tool.to_definition();
3635 if def.hints().narration_noun.is_some() {
3638 continue;
3639 }
3640
3641 let call = ToolCall {
3642 id: "call_narration_audit".to_string(),
3643 name: tool.name().to_string(),
3644 arguments: serde_json::json!({}),
3645 };
3646 if cap
3649 .narrate(Some(&def), &call, ToolNarrationPhase::Started, None, ctx)
3650 .is_none()
3651 {
3652 missing.push(format!("{cap_id}::{}", tool.name()));
3653 }
3654 }
3655 }
3656
3657 assert!(
3658 missing.is_empty(),
3659 "These built-in tools fall back to raw tool-call presentation. Implement \
3660 `Tool::narrate` (see knowledge/execution/tool-narration.md), set a `narration_noun` hint, \
3661 or add a documented entry to GENERIC_NARRATION_ALLOWLIST: {missing:?}"
3662 );
3663 }
3664
3665 #[test]
3666 fn test_capability_registry_blueprint_with_capability() {
3667 struct BlueprintProviderCapability;
3668
3669 impl Capability for BlueprintProviderCapability {
3670 fn id(&self) -> &str {
3671 "blueprint_provider"
3672 }
3673 fn name(&self) -> &str {
3674 "Blueprint Provider"
3675 }
3676 fn description(&self) -> &str {
3677 "Capability that provides a blueprint for tests"
3678 }
3679 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3680 vec![AgentBlueprint {
3681 id: "test_blueprint",
3682 name: "Test Blueprint",
3683 description: "Blueprint for capability registry tests",
3684 model: BlueprintModel::Inherit,
3685 system_prompt: "Test prompt",
3686 tools: vec![],
3687 max_turns: None,
3688 config_schema: None,
3689 }]
3690 }
3691 }
3692
3693 let mut registry = CapabilityRegistry::new();
3694 registry.register(BlueprintProviderCapability);
3695
3696 let (capability_id, blueprint) = registry
3697 .blueprint_with_capability("test_blueprint")
3698 .expect("blueprint should resolve with capability id");
3699 assert_eq!(capability_id, "blueprint_provider");
3700 assert_eq!(blueprint.id, "test_blueprint");
3701 }
3702
3703 #[test]
3704 fn test_capability_registry_builder() {
3705 let registry = CapabilityRegistry::builder()
3706 .capability(NoopCapability)
3707 .capability(CurrentTimeCapability)
3708 .build();
3709
3710 assert!(registry.has("noop"));
3711 assert!(registry.has("current_time"));
3712 assert_eq!(registry.len(), 2);
3713 }
3714
3715 #[test]
3716 fn test_capability_status() {
3717 let registry = CapabilityRegistry::with_builtins();
3718
3719 let current_time = registry.get("current_time").unwrap();
3720 assert_eq!(current_time.status(), CapabilityStatus::Available);
3721
3722 let research = registry.get("research").unwrap();
3723 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3724 }
3725
3726 #[test]
3727 fn test_capability_icons_and_categories() {
3728 let registry = CapabilityRegistry::with_builtins();
3729
3730 let noop = registry.get("noop").unwrap();
3731 assert_eq!(noop.icon(), Some("circle-off"));
3732 assert_eq!(noop.category(), Some("Testing"));
3733
3734 let current_time = registry.get("current_time").unwrap();
3735 assert_eq!(current_time.icon(), Some("clock"));
3736 assert_eq!(current_time.category(), Some("Core"));
3737 }
3738
3739 #[test]
3740 fn test_system_prompt_preview_default_delegates_to_addition() {
3741 let registry = CapabilityRegistry::with_builtins();
3742
3743 let test_math = registry.get("test_math").unwrap();
3745 assert_eq!(
3746 test_math.system_prompt_preview().as_deref(),
3747 test_math.system_prompt_addition()
3748 );
3749
3750 let current_time = registry.get("current_time").unwrap();
3752 assert!(current_time.system_prompt_preview().is_none());
3753 assert!(current_time.system_prompt_addition().is_none());
3754 }
3755
3756 #[test]
3757 fn test_system_prompt_preview_dynamic_capability() {
3758 let registry = CapabilityRegistry::with_builtins();
3759 let cap = registry.get("agent_instructions").unwrap();
3760
3761 assert!(cap.system_prompt_addition().is_none());
3763 assert!(cap.system_prompt_preview().is_some());
3764 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3765 }
3766
3767 #[tokio::test]
3772 async fn test_apply_capabilities_empty() {
3773 let registry = CapabilityRegistry::with_builtins();
3774 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3775
3776 let applied =
3777 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3778
3779 assert_eq!(
3780 applied.runtime_agent.system_prompt,
3781 base_runtime_agent.system_prompt
3782 );
3783 assert!(applied.tool_registry.is_empty());
3784 assert!(applied.applied_ids.is_empty());
3785 }
3786
3787 #[tokio::test]
3788 async fn test_apply_capabilities_noop() {
3789 let registry = CapabilityRegistry::with_builtins();
3790 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3791
3792 let applied = apply_capabilities(
3793 base_runtime_agent.clone(),
3794 &["noop".to_string()],
3795 ®istry,
3796 &test_ctx(),
3797 )
3798 .await;
3799
3800 assert_eq!(
3802 applied.runtime_agent.system_prompt,
3803 base_runtime_agent.system_prompt
3804 );
3805 assert!(applied.tool_registry.is_empty());
3806 assert_eq!(applied.applied_ids, vec!["noop"]);
3807 }
3808
3809 #[tokio::test]
3810 async fn test_apply_capabilities_current_time() {
3811 let registry = CapabilityRegistry::with_builtins();
3812 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3813
3814 let applied = apply_capabilities(
3815 base_runtime_agent.clone(),
3816 &["current_time".to_string()],
3817 ®istry,
3818 &test_ctx(),
3819 )
3820 .await;
3821
3822 assert!(
3826 applied
3827 .runtime_agent
3828 .system_prompt
3829 .contains(FACTS_DYNAMIC_NOTE),
3830 "current_time should contribute the dynamic-facts note"
3831 );
3832 assert!(
3833 applied
3834 .runtime_agent
3835 .system_prompt
3836 .contains(&base_runtime_agent.system_prompt),
3837 "base prompt is preserved"
3838 );
3839 assert!(applied.tool_registry.has("get_current_time"));
3840 assert_eq!(applied.tool_registry.len(), 1);
3841 assert_eq!(applied.applied_ids, vec!["current_time"]);
3842 }
3843
3844 #[tokio::test]
3845 async fn test_apply_capabilities_skips_coming_soon() {
3846 let registry = CapabilityRegistry::with_builtins();
3847 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3848
3849 let applied = apply_capabilities(
3851 base_runtime_agent.clone(),
3852 &["research".to_string()],
3853 ®istry,
3854 &test_ctx(),
3855 )
3856 .await;
3857
3858 assert_eq!(
3860 applied.runtime_agent.system_prompt,
3861 base_runtime_agent.system_prompt
3862 );
3863 assert!(applied.applied_ids.is_empty()); }
3865
3866 #[tokio::test]
3867 async fn test_apply_capabilities_multiple() {
3868 let registry = CapabilityRegistry::with_builtins();
3869 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3870
3871 let applied = apply_capabilities(
3872 base_runtime_agent.clone(),
3873 &["noop".to_string(), "current_time".to_string()],
3874 ®istry,
3875 &test_ctx(),
3876 )
3877 .await;
3878
3879 assert!(applied.tool_registry.has("get_current_time"));
3880 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3881 }
3882
3883 #[tokio::test]
3884 async fn test_apply_capabilities_preserves_order() {
3885 let registry = CapabilityRegistry::with_builtins();
3886 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3887
3888 let applied = apply_capabilities(
3890 base_runtime_agent,
3891 &["current_time".to_string(), "noop".to_string()],
3892 ®istry,
3893 &test_ctx(),
3894 )
3895 .await;
3896
3897 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3898 }
3899
3900 #[tokio::test]
3901 async fn test_apply_capabilities_test_math() {
3902 let registry = CapabilityRegistry::with_builtins();
3903 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3904
3905 let applied = apply_capabilities(
3906 base_runtime_agent.clone(),
3907 &["test_math".to_string()],
3908 ®istry,
3909 &test_ctx(),
3910 )
3911 .await;
3912
3913 assert!(
3915 !applied
3916 .runtime_agent
3917 .system_prompt
3918 .contains("<capability id=\"test_math\">")
3919 );
3920 assert!(
3922 applied
3923 .runtime_agent
3924 .system_prompt
3925 .contains("You are a helpful assistant.")
3926 );
3927 assert!(applied.tool_registry.has("add"));
3928 assert!(applied.tool_registry.has("subtract"));
3929 assert!(applied.tool_registry.has("multiply"));
3930 assert!(applied.tool_registry.has("divide"));
3931 assert_eq!(applied.tool_registry.len(), 4);
3932 }
3933
3934 #[tokio::test]
3935 async fn test_apply_capabilities_test_weather() {
3936 let registry = CapabilityRegistry::with_builtins();
3937 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3938
3939 let applied = apply_capabilities(
3940 base_runtime_agent.clone(),
3941 &["test_weather".to_string()],
3942 ®istry,
3943 &test_ctx(),
3944 )
3945 .await;
3946
3947 assert!(
3949 !applied
3950 .runtime_agent
3951 .system_prompt
3952 .contains("<capability id=\"test_weather\">")
3953 );
3954 assert!(applied.tool_registry.has("get_weather"));
3955 assert!(applied.tool_registry.has("get_forecast"));
3956 assert_eq!(applied.tool_registry.len(), 2);
3957 }
3958
3959 #[tokio::test]
3960 async fn test_apply_capabilities_test_math_and_test_weather() {
3961 let registry = CapabilityRegistry::with_builtins();
3962 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3963
3964 let applied = apply_capabilities(
3965 base_runtime_agent.clone(),
3966 &["test_math".to_string(), "test_weather".to_string()],
3967 ®istry,
3968 &test_ctx(),
3969 )
3970 .await;
3971
3972 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3975 assert!(applied.tool_registry.has("get_weather"));
3976 }
3977
3978 #[tokio::test]
3979 async fn test_apply_capabilities_stateless_todo_list() {
3980 let registry = CapabilityRegistry::with_builtins();
3981 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3982
3983 let applied = apply_capabilities(
3984 base_runtime_agent.clone(),
3985 &["stateless_todo_list".to_string()],
3986 ®istry,
3987 &test_ctx(),
3988 )
3989 .await;
3990
3991 assert!(
3993 applied
3994 .runtime_agent
3995 .system_prompt
3996 .contains("Task Management")
3997 );
3998 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3999 assert!(applied.tool_registry.has("write_todos"));
4000 assert_eq!(applied.tool_registry.len(), 1);
4001 }
4002
4003 #[tokio::test]
4004 async fn test_apply_capabilities_web_fetch() {
4005 let registry = CapabilityRegistry::with_builtins();
4006 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
4007
4008 let applied = apply_capabilities(
4009 base_runtime_agent.clone(),
4010 &["web_fetch".to_string()],
4011 ®istry,
4012 &test_ctx(),
4013 )
4014 .await;
4015
4016 assert!(
4018 applied
4019 .runtime_agent
4020 .system_prompt
4021 .contains(&base_runtime_agent.system_prompt)
4022 );
4023 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
4024 assert!(applied.tool_registry.has("web_fetch"));
4025 assert_eq!(applied.tool_registry.len(), 1);
4026 }
4027
4028 #[tokio::test]
4033 async fn test_xml_tags_wrap_capability_prompts() {
4034 let registry = CapabilityRegistry::with_builtins();
4035 let collected =
4036 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
4037 .await;
4038
4039 assert_eq!(collected.system_prompt_parts.len(), 1);
4040 let part = &collected.system_prompt_parts[0];
4041 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
4042 assert!(part.ends_with("</capability>"));
4043 assert!(part.contains("Task Management"));
4044 }
4045
4046 #[tokio::test]
4047 async fn test_xml_tags_multiple_capabilities() {
4048 let registry = CapabilityRegistry::with_builtins();
4049 let collected = collect_capabilities(
4050 &[
4051 "stateless_todo_list".to_string(),
4052 "session_schedule".to_string(),
4053 ],
4054 ®istry,
4055 &test_ctx(),
4056 )
4057 .await;
4058
4059 assert_eq!(collected.system_prompt_parts.len(), 2);
4060 assert!(
4061 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
4062 );
4063 assert!(
4064 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
4065 );
4066
4067 let prefix = collected.system_prompt_prefix().unwrap();
4068 assert!(prefix.contains("</capability>\n\n<capability"));
4070 }
4071
4072 #[tokio::test]
4073 async fn test_xml_tags_system_prompt_wrapping() {
4074 let registry = CapabilityRegistry::with_builtins();
4075 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4076
4077 let applied = apply_capabilities(
4078 base,
4079 &["stateless_todo_list".to_string()],
4080 ®istry,
4081 &test_ctx(),
4082 )
4083 .await;
4084
4085 let prompt = &applied.runtime_agent.system_prompt;
4086 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4087 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
4089 assert!(prompt.contains("</capability>"));
4090 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4092 }
4093
4094 #[tokio::test]
4095 async fn test_no_xml_wrapping_without_capabilities() {
4096 let registry = CapabilityRegistry::with_builtins();
4097 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4098
4099 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
4100
4101 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4103 assert!(
4104 !applied
4105 .runtime_agent
4106 .system_prompt
4107 .contains("<system-prompt>")
4108 );
4109 }
4110
4111 #[tokio::test]
4112 async fn test_no_xml_wrapping_for_noop_capability() {
4113 let registry = CapabilityRegistry::with_builtins();
4114 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4115
4116 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
4118
4119 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4120 assert!(
4121 !applied
4122 .runtime_agent
4123 .system_prompt
4124 .contains("<system-prompt>")
4125 );
4126 }
4127
4128 #[tokio::test]
4133 async fn test_collect_capabilities_includes_mounts() {
4134 let registry = CapabilityRegistry::with_builtins();
4135
4136 let collected =
4137 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4138
4139 assert!(!collected.mounts.is_empty());
4140 assert_eq!(collected.mounts.len(), 1);
4141 assert_eq!(collected.mounts[0].path, "/samples");
4142 assert!(collected.mounts[0].is_readonly());
4143 }
4144
4145 #[tokio::test]
4146 async fn test_collect_capabilities_empty_mounts_by_default() {
4147 let registry = CapabilityRegistry::with_builtins();
4148
4149 let collected =
4151 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4152
4153 assert!(collected.mounts.is_empty());
4154 }
4155
4156 #[tokio::test]
4157 async fn test_dynamic_facts_add_note_without_static_block() {
4158 let registry = CapabilityRegistry::with_builtins();
4162 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4163 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4164 let prompt = collected.system_prompt_parts.join("\n");
4165 assert!(
4166 prompt.contains(FACTS_DYNAMIC_NOTE),
4167 "dynamic-facts note should be in the cached prompt"
4168 );
4169 assert!(
4170 !prompt.contains("<facts>\n"),
4171 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
4172 );
4173 }
4174
4175 #[tokio::test]
4176 async fn test_static_facts_fold_into_prompt() {
4177 struct StaticFactCap;
4178 impl Capability for StaticFactCap {
4179 fn id(&self) -> &str {
4180 "test_static_fact"
4181 }
4182 fn name(&self) -> &str {
4183 "Static Fact"
4184 }
4185 fn description(&self) -> &str {
4186 "test"
4187 }
4188 fn status(&self) -> CapabilityStatus {
4189 CapabilityStatus::Available
4190 }
4191 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
4192 vec![Fact::stat("workspace_root", "/workspace")]
4193 }
4194 }
4195 let mut registry = CapabilityRegistry::new();
4196 registry.register(StaticFactCap);
4197 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
4198 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4199 let prompt = collected.system_prompt_parts.join("\n");
4200 assert!(
4201 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
4202 "static fact should fold into the cached prompt; got: {prompt}"
4203 );
4204 assert!(
4205 !prompt.contains(FACTS_DYNAMIC_NOTE),
4206 "no dynamic note when only static facts exist"
4207 );
4208 }
4209
4210 #[test]
4211 fn test_collect_dynamic_facts_returns_current_time() {
4212 let registry = CapabilityRegistry::with_builtins();
4213 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4214 let facts = collect_dynamic_facts(
4215 &configs,
4216 ®istry,
4217 None,
4218 &FactsContext::new(SessionId::new()),
4219 );
4220 assert_eq!(facts.len(), 1);
4221 assert_eq!(facts[0].key, "current_time");
4222 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4223 }
4224
4225 #[tokio::test]
4226 async fn test_collect_capabilities_combines_mounts() {
4227 let registry = CapabilityRegistry::with_builtins();
4228
4229 let collected = collect_capabilities(
4232 &["sample_data".to_string(), "current_time".to_string()],
4233 ®istry,
4234 &test_ctx(),
4235 )
4236 .await;
4237
4238 assert_eq!(collected.mounts.len(), 1);
4239 assert!(
4241 collected
4242 .applied_ids
4243 .iter()
4244 .any(|id| id == "session_file_system")
4245 );
4246 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4247 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4248 }
4249
4250 #[test]
4251 fn test_sample_data_capability() {
4252 let registry = CapabilityRegistry::with_builtins();
4253 let cap = registry.get("sample_data").unwrap();
4254
4255 assert_eq!(cap.id(), "sample_data");
4256 assert_eq!(cap.name(), "Sample Data");
4257 assert_eq!(cap.status(), CapabilityStatus::Available);
4258
4259 assert!(cap.system_prompt_addition().is_some());
4261 assert!(cap.tools().is_empty());
4262
4263 assert!(!cap.mounts().is_empty());
4265 }
4266
4267 #[test]
4272 fn test_resolve_dependencies_empty() {
4273 let registry = CapabilityRegistry::with_builtins();
4274
4275 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4276
4277 assert!(resolved.resolved_ids.is_empty());
4278 assert!(resolved.added_as_dependencies.is_empty());
4279 assert!(resolved.user_selected.is_empty());
4280 }
4281
4282 #[test]
4283 fn test_resolve_dependencies_no_deps() {
4284 let registry = CapabilityRegistry::with_builtins();
4285
4286 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4288
4289 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4290 assert!(resolved.added_as_dependencies.is_empty());
4291 }
4292
4293 #[test]
4294 fn test_resolve_dependencies_with_deps() {
4295 let registry = CapabilityRegistry::with_builtins();
4296
4297 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4299
4300 assert_eq!(resolved.resolved_ids.len(), 2);
4302 let fs_pos = resolved
4303 .resolved_ids
4304 .iter()
4305 .position(|id| id == "session_file_system")
4306 .unwrap();
4307 let sd_pos = resolved
4308 .resolved_ids
4309 .iter()
4310 .position(|id| id == "sample_data")
4311 .unwrap();
4312 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4313
4314 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4316 }
4317
4318 #[test]
4319 fn test_resolve_dependencies_already_selected() {
4320 let registry = CapabilityRegistry::with_builtins();
4321
4322 let resolved = resolve_dependencies(
4324 &["session_file_system".to_string(), "sample_data".to_string()],
4325 ®istry,
4326 )
4327 .unwrap();
4328
4329 assert_eq!(resolved.resolved_ids.len(), 2);
4330 assert!(resolved.added_as_dependencies.is_empty());
4332 }
4333
4334 #[test]
4335 fn test_resolve_dependencies_preserves_order() {
4336 let registry = CapabilityRegistry::with_builtins();
4337
4338 let resolved =
4340 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4341 .unwrap();
4342
4343 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4344 }
4345
4346 #[test]
4347 fn test_resolve_dependencies_unknown_capability() {
4348 let registry = CapabilityRegistry::with_builtins();
4349
4350 let resolved =
4352 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4353
4354 assert!(resolved.resolved_ids.is_empty());
4355 }
4356
4357 #[test]
4358 fn test_get_dependencies() {
4359 let registry = CapabilityRegistry::with_builtins();
4360
4361 let deps = get_dependencies("sample_data", ®istry);
4363 assert_eq!(deps, vec!["session_file_system"]);
4364
4365 let deps = get_dependencies("current_time", ®istry);
4367 assert!(deps.is_empty());
4368
4369 let deps = get_dependencies("unknown", ®istry);
4371 assert!(deps.is_empty());
4372 }
4373
4374 #[test]
4375 fn test_sample_data_has_dependency() {
4376 let registry = CapabilityRegistry::with_builtins();
4377 let cap = registry.get("sample_data").unwrap();
4378
4379 let deps = cap.dependencies();
4380 assert_eq!(deps.len(), 1);
4381 assert_eq!(deps[0], "session_file_system");
4382 }
4383
4384 #[test]
4385 fn test_noop_has_no_dependencies() {
4386 let registry = CapabilityRegistry::with_builtins();
4387 let cap = registry.get("noop").unwrap();
4388
4389 assert!(cap.dependencies().is_empty());
4390 }
4391
4392 #[test]
4396 fn test_circular_dependency_error() {
4397 struct CapA;
4399 struct CapB;
4400
4401 impl Capability for CapA {
4402 fn id(&self) -> &str {
4403 "test_cap_a"
4404 }
4405 fn name(&self) -> &str {
4406 "Test A"
4407 }
4408 fn description(&self) -> &str {
4409 "Test capability A"
4410 }
4411 fn dependencies(&self) -> Vec<&'static str> {
4412 vec!["test_cap_b"]
4413 }
4414 }
4415
4416 impl Capability for CapB {
4417 fn id(&self) -> &str {
4418 "test_cap_b"
4419 }
4420 fn name(&self) -> &str {
4421 "Test B"
4422 }
4423 fn description(&self) -> &str {
4424 "Test capability B"
4425 }
4426 fn dependencies(&self) -> Vec<&'static str> {
4427 vec!["test_cap_a"]
4428 }
4429 }
4430
4431 let mut registry = CapabilityRegistry::new();
4432 registry.register(CapA);
4433 registry.register(CapB);
4434
4435 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4436
4437 assert!(result.is_err());
4438 match result.unwrap_err() {
4439 DependencyError::CircularDependency { capability_id, .. } => {
4440 assert_eq!(capability_id, "test_cap_a");
4441 }
4442 _ => panic!("Expected CircularDependency error"),
4443 }
4444 }
4445
4446 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4451
4452 struct FilterTestCapability {
4454 priority: i32,
4455 }
4456
4457 impl Capability for FilterTestCapability {
4458 fn id(&self) -> &str {
4459 "filter_test"
4460 }
4461 fn name(&self) -> &str {
4462 "Filter Test"
4463 }
4464 fn description(&self) -> &str {
4465 "Test capability with message filter"
4466 }
4467 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4468 Some(Arc::new(FilterTestProvider {
4469 priority: self.priority,
4470 }))
4471 }
4472 }
4473
4474 struct FilterTestProvider {
4475 priority: i32,
4476 }
4477
4478 impl MessageFilterProvider for FilterTestProvider {
4479 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4480 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4482 query
4483 .filters
4484 .push(MessageFilter::Search(search.to_string()));
4485 }
4486 }
4487
4488 fn priority(&self) -> i32 {
4489 self.priority
4490 }
4491 }
4492
4493 #[tokio::test]
4494 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4495 let registry = CapabilityRegistry::with_builtins();
4496 let configs = vec![AgentCapabilityConfig {
4497 capability_ref: CapabilityId::new("current_time"),
4498 config: serde_json::json!({}),
4499 }];
4500
4501 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4502
4503 assert!(collected.message_filter_providers.is_empty());
4504 assert!(!collected.has_message_filters());
4505 }
4506
4507 #[tokio::test]
4508 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4509 let mut registry = CapabilityRegistry::new();
4510 registry.register(FilterTestCapability { priority: 0 });
4511
4512 let configs = vec![AgentCapabilityConfig {
4513 capability_ref: CapabilityId::new("filter_test"),
4514 config: serde_json::json!({ "search": "hello" }),
4515 }];
4516
4517 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4518
4519 assert_eq!(collected.message_filter_providers.len(), 1);
4520 assert!(collected.has_message_filters());
4521 }
4522
4523 #[tokio::test]
4524 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4525 struct HighPriorityCapability;
4527 struct LowPriorityCapability;
4528
4529 impl Capability for HighPriorityCapability {
4530 fn id(&self) -> &str {
4531 "high_priority"
4532 }
4533 fn name(&self) -> &str {
4534 "High Priority"
4535 }
4536 fn description(&self) -> &str {
4537 "Test"
4538 }
4539 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4540 Some(Arc::new(FilterTestProvider { priority: 10 }))
4541 }
4542 }
4543
4544 impl Capability for LowPriorityCapability {
4545 fn id(&self) -> &str {
4546 "low_priority"
4547 }
4548 fn name(&self) -> &str {
4549 "Low Priority"
4550 }
4551 fn description(&self) -> &str {
4552 "Test"
4553 }
4554 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4555 Some(Arc::new(FilterTestProvider { priority: -5 }))
4556 }
4557 }
4558
4559 let mut registry = CapabilityRegistry::new();
4560 registry.register(HighPriorityCapability);
4561 registry.register(LowPriorityCapability);
4562
4563 let configs = vec![
4565 AgentCapabilityConfig {
4566 capability_ref: CapabilityId::new("high_priority"),
4567 config: serde_json::json!({}),
4568 },
4569 AgentCapabilityConfig {
4570 capability_ref: CapabilityId::new("low_priority"),
4571 config: serde_json::json!({}),
4572 },
4573 ];
4574
4575 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4576
4577 assert_eq!(collected.message_filter_providers.len(), 2);
4579 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4580 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4581 }
4582
4583 #[tokio::test]
4584 async fn test_collected_capabilities_apply_message_filters() {
4585 let mut registry = CapabilityRegistry::new();
4586 registry.register(FilterTestCapability { priority: 0 });
4587
4588 let configs = vec![AgentCapabilityConfig {
4589 capability_ref: CapabilityId::new("filter_test"),
4590 config: serde_json::json!({ "search": "test_query" }),
4591 }];
4592
4593 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4594
4595 let session_id: SessionId = Uuid::now_v7().into();
4597 let mut query = MessageQuery::new(session_id);
4598
4599 collected.apply_message_filters(&mut query);
4600
4601 assert_eq!(query.filters.len(), 1);
4603 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4604 }
4605
4606 #[tokio::test]
4607 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4608 struct SearchCapability {
4609 id: &'static str,
4610 search_term: &'static str,
4611 priority: i32,
4612 }
4613
4614 struct SearchProvider {
4615 search_term: &'static str,
4616 priority: i32,
4617 }
4618
4619 impl MessageFilterProvider for SearchProvider {
4620 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4621 query
4622 .filters
4623 .push(MessageFilter::Search(self.search_term.to_string()));
4624 }
4625
4626 fn priority(&self) -> i32 {
4627 self.priority
4628 }
4629 }
4630
4631 impl Capability for SearchCapability {
4632 fn id(&self) -> &str {
4633 self.id
4634 }
4635 fn name(&self) -> &str {
4636 "Search"
4637 }
4638 fn description(&self) -> &str {
4639 "Test"
4640 }
4641 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4642 Some(Arc::new(SearchProvider {
4643 search_term: self.search_term,
4644 priority: self.priority,
4645 }))
4646 }
4647 }
4648
4649 let mut registry = CapabilityRegistry::new();
4650 registry.register(SearchCapability {
4651 id: "cap_a",
4652 search_term: "alpha",
4653 priority: 5,
4654 });
4655 registry.register(SearchCapability {
4656 id: "cap_b",
4657 search_term: "beta",
4658 priority: 1,
4659 });
4660 registry.register(SearchCapability {
4661 id: "cap_c",
4662 search_term: "gamma",
4663 priority: 10,
4664 });
4665
4666 let configs = vec![
4667 AgentCapabilityConfig {
4668 capability_ref: CapabilityId::new("cap_a"),
4669 config: serde_json::json!({}),
4670 },
4671 AgentCapabilityConfig {
4672 capability_ref: CapabilityId::new("cap_b"),
4673 config: serde_json::json!({}),
4674 },
4675 AgentCapabilityConfig {
4676 capability_ref: CapabilityId::new("cap_c"),
4677 config: serde_json::json!({}),
4678 },
4679 ];
4680
4681 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4682
4683 let session_id: SessionId = Uuid::now_v7().into();
4684 let mut query = MessageQuery::new(session_id);
4685
4686 collected.apply_message_filters(&mut query);
4687
4688 assert_eq!(query.filters.len(), 3);
4690 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4691 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4692 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4693 }
4694
4695 #[test]
4696 fn test_capability_without_message_filter_returns_none() {
4697 let registry = CapabilityRegistry::with_builtins();
4698
4699 let noop = registry.get("noop").unwrap();
4700 assert!(noop.message_filter_provider().is_none());
4701
4702 let current_time = registry.get("current_time").unwrap();
4703 assert!(current_time.message_filter_provider().is_none());
4704 }
4705
4706 #[tokio::test]
4707 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4708 let mut registry = CapabilityRegistry::new();
4709 registry.register(FilterTestCapability { priority: 0 });
4710
4711 let test_config = serde_json::json!({
4712 "search": "custom_search",
4713 "extra_field": 42
4714 });
4715
4716 let configs = vec![AgentCapabilityConfig {
4717 capability_ref: CapabilityId::new("filter_test"),
4718 config: test_config.clone(),
4719 }];
4720
4721 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4722
4723 assert_eq!(collected.message_filter_providers.len(), 1);
4725 let (_, stored_config) = &collected.message_filter_providers[0];
4726 assert_eq!(*stored_config, test_config);
4727 }
4728
4729 #[test]
4734 fn test_collect_message_filters_only_collects_filters() {
4735 let mut registry = CapabilityRegistry::new();
4736 registry.register(FilterTestCapability { priority: 0 });
4737
4738 let configs = vec![AgentCapabilityConfig {
4739 capability_ref: CapabilityId::new("filter_test"),
4740 config: serde_json::json!({ "search": "test_query" }),
4741 }];
4742
4743 let collected = collect_message_filters_only(&configs, ®istry);
4744
4745 let session_id: SessionId = Uuid::now_v7().into();
4746 let mut query = MessageQuery::new(session_id);
4747 collected.apply_message_filters(&mut query);
4748
4749 assert_eq!(query.filters.len(), 1);
4750 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4751 }
4752
4753 #[test]
4754 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4755 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4756
4757 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4759 assert_eq!(with["compaction_active"], serde_json::json!(true));
4760 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4761
4762 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4763 assert!(without.get("compaction_active").is_none());
4764
4765 let other = message_filter_config_for("other", &base, true);
4767 assert!(other.get("compaction_active").is_none());
4768
4769 let null_base = message_filter_config_for(
4771 INFINITY_CONTEXT_CAPABILITY_ID,
4772 &serde_json::Value::Null,
4773 true,
4774 );
4775 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4776 }
4777
4778 #[test]
4779 fn test_infinity_context_defers_to_compaction_end_to_end() {
4780 use crate::message::Message;
4781
4782 let mut registry = CapabilityRegistry::new();
4783 registry.register(InfinityContextCapability);
4784 registry.register(CompactionCapability);
4785
4786 let tight = serde_json::json!({
4787 "context_budget_tokens": 1,
4788 "min_recent_messages": 1
4789 });
4790
4791 let solo = vec![AgentCapabilityConfig {
4793 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4794 config: tight.clone(),
4795 }];
4796 let mut messages = vec![
4797 Message::user("task"),
4798 Message::assistant("old ".repeat(400)),
4799 Message::user("recent"),
4800 ];
4801 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4802 assert!(
4803 messages
4804 .iter()
4805 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4806 "infinity context alone should trim and notice"
4807 );
4808
4809 let both = vec![
4811 AgentCapabilityConfig {
4812 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4813 config: tight,
4814 },
4815 AgentCapabilityConfig {
4816 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4817 config: serde_json::json!({}),
4818 },
4819 ];
4820 let mut messages = vec![
4821 Message::user("task"),
4822 Message::assistant("old ".repeat(400)),
4823 Message::user("recent"),
4824 ];
4825 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4826 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4827 assert!(
4828 messages
4829 .iter()
4830 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4831 "no hidden-history notice when compaction is the active reducer"
4832 );
4833 }
4834
4835 #[test]
4836 fn test_compaction_is_enabled_detects_compaction() {
4837 let mut registry = CapabilityRegistry::new();
4838 registry.register(CompactionCapability);
4839
4840 let with_compaction = vec![AgentCapabilityConfig {
4841 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4842 config: serde_json::json!({}),
4843 }];
4844 assert!(compaction_is_enabled(&with_compaction, ®istry));
4845
4846 let without = vec![AgentCapabilityConfig {
4847 capability_ref: CapabilityId::new("current_time"),
4848 config: serde_json::json!({}),
4849 }];
4850 assert!(!compaction_is_enabled(&without, ®istry));
4851 }
4852
4853 #[test]
4854 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4855 let registry = CapabilityRegistry::new();
4856
4857 let configs = vec![AgentCapabilityConfig {
4858 capability_ref: CapabilityId::new("nonexistent"),
4859 config: serde_json::json!({}),
4860 }];
4861
4862 let collected = collect_message_filters_only(&configs, ®istry);
4863 assert!(collected.message_filter_providers.is_empty());
4864 }
4865
4866 #[test]
4867 fn test_collect_message_filters_only_preserves_priority_order() {
4868 struct PriorityFilterCap {
4869 id: &'static str,
4870 search_term: &'static str,
4871 priority: i32,
4872 }
4873
4874 struct PriorityFilterProvider {
4875 search_term: &'static str,
4876 priority: i32,
4877 }
4878
4879 impl Capability for PriorityFilterCap {
4880 fn id(&self) -> &str {
4881 self.id
4882 }
4883 fn name(&self) -> &str {
4884 self.id
4885 }
4886 fn description(&self) -> &str {
4887 "priority test"
4888 }
4889 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4890 Some(Arc::new(PriorityFilterProvider {
4891 search_term: self.search_term,
4892 priority: self.priority,
4893 }))
4894 }
4895 }
4896
4897 impl MessageFilterProvider for PriorityFilterProvider {
4898 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4899 query
4900 .filters
4901 .push(MessageFilter::Search(self.search_term.to_string()));
4902 }
4903 fn priority(&self) -> i32 {
4904 self.priority
4905 }
4906 }
4907
4908 let mut registry = CapabilityRegistry::new();
4909 registry.register(PriorityFilterCap {
4910 id: "gamma",
4911 search_term: "gamma",
4912 priority: 10,
4913 });
4914 registry.register(PriorityFilterCap {
4915 id: "alpha",
4916 search_term: "alpha",
4917 priority: 5,
4918 });
4919 registry.register(PriorityFilterCap {
4920 id: "beta",
4921 search_term: "beta",
4922 priority: 1,
4923 });
4924
4925 let configs = vec![
4926 AgentCapabilityConfig {
4927 capability_ref: CapabilityId::new("gamma"),
4928 config: serde_json::json!({}),
4929 },
4930 AgentCapabilityConfig {
4931 capability_ref: CapabilityId::new("alpha"),
4932 config: serde_json::json!({}),
4933 },
4934 AgentCapabilityConfig {
4935 capability_ref: CapabilityId::new("beta"),
4936 config: serde_json::json!({}),
4937 },
4938 ];
4939
4940 let collected = collect_message_filters_only(&configs, ®istry);
4941
4942 let session_id: SessionId = Uuid::now_v7().into();
4943 let mut query = MessageQuery::new(session_id);
4944 collected.apply_message_filters(&mut query);
4945
4946 assert_eq!(query.filters.len(), 3);
4948 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4949 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4950 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4951 }
4952
4953 #[test]
4954 fn test_collect_message_filters_only_post_load_invoked() {
4955 use crate::message::Message;
4956
4957 struct PostLoadCap;
4958 struct PostLoadProvider;
4959
4960 impl Capability for PostLoadCap {
4961 fn id(&self) -> &str {
4962 "post_load_test"
4963 }
4964 fn name(&self) -> &str {
4965 "PostLoad Test"
4966 }
4967 fn description(&self) -> &str {
4968 "test"
4969 }
4970 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4971 Some(Arc::new(PostLoadProvider))
4972 }
4973 }
4974
4975 impl MessageFilterProvider for PostLoadProvider {
4976 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4977 fn priority(&self) -> i32 {
4978 0
4979 }
4980 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4981 messages.reverse();
4983 }
4984 }
4985
4986 let mut registry = CapabilityRegistry::new();
4987 registry.register(PostLoadCap);
4988
4989 let configs = vec![AgentCapabilityConfig {
4990 capability_ref: CapabilityId::new("post_load_test"),
4991 config: serde_json::json!({}),
4992 }];
4993
4994 let collected = collect_message_filters_only(&configs, ®istry);
4995
4996 let mut messages = vec![Message::user("first"), Message::user("second")];
4997 collected.apply_post_load_filters(&mut messages);
4998
4999 assert_eq!(messages[0].text(), Some("second"));
5001 assert_eq!(messages[1].text(), Some("first"));
5002 }
5003
5004 #[test]
5005 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
5006 use crate::tool_types::ToolCall;
5007
5008 fn tool_heavy_messages() -> Vec<Message> {
5009 let mut messages = vec![Message::user("inspect files repeatedly")];
5010 for index in 0..9 {
5011 let call_id = format!("call_{index}");
5012 messages.push(Message::assistant_with_tools(
5013 "",
5014 vec![ToolCall {
5015 id: call_id.clone(),
5016 name: "read_file".to_string(),
5017 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
5018 }],
5019 ));
5020 messages.push(Message::tool_result(
5021 call_id,
5022 Some(serde_json::json!({
5023 "path": "/workspace/src/lib.rs",
5024 "content": format!("{}{}", "large file line\n".repeat(1000), index),
5025 "total_lines": 1000,
5026 "lines_shown": {"start": 1, "end": 1000},
5027 "truncated": false
5028 })),
5029 None,
5030 ));
5031 }
5032 messages
5033 }
5034
5035 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
5036 messages[2]
5037 .tool_result_content()
5038 .and_then(|result| result.result.as_ref())
5039 .and_then(|result| result.get("masked"))
5040 .and_then(|masked| masked.as_bool())
5041 .unwrap_or(false)
5042 }
5043
5044 let mut registry = CapabilityRegistry::new();
5045 registry.register(CompactionCapability);
5046 let context = ModelViewContext {
5047 session_id: SessionId::new(),
5048 prior_usage: None,
5049 };
5050
5051 let no_compaction = collect_model_view_providers(&[], ®istry, None);
5052 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
5053 assert!(!first_tool_result_is_masked(&unmasked));
5054
5055 let compaction = collect_model_view_providers(
5056 &[AgentCapabilityConfig {
5057 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
5058 config: serde_json::json!({}),
5059 }],
5060 ®istry,
5061 None,
5062 );
5063 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
5064 assert!(first_tool_result_is_masked(&masked));
5065 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
5066 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
5067 }
5068
5069 struct DelegatingFilterCap {
5072 id: &'static str,
5073 inner: std::sync::Arc<InnerFilterCap>,
5074 }
5075 struct InnerFilterCap;
5076
5077 impl Capability for InnerFilterCap {
5078 fn id(&self) -> &str {
5079 "inner_filter"
5080 }
5081 fn name(&self) -> &str {
5082 "Inner Filter"
5083 }
5084 fn description(&self) -> &str {
5085 "inner"
5086 }
5087 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5088 Some(std::sync::Arc::new(SentinelFilter))
5089 }
5090 }
5091 struct SentinelFilter;
5092 impl MessageFilterProvider for SentinelFilter {
5093 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
5094 }
5095 impl Capability for DelegatingFilterCap {
5096 fn id(&self) -> &str {
5097 self.id
5098 }
5099 fn name(&self) -> &str {
5100 "Delegating Filter"
5101 }
5102 fn description(&self) -> &str {
5103 "delegating"
5104 }
5105 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5106 None }
5108 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5109 Some(&*self.inner)
5110 }
5111 }
5112
5113 #[test]
5114 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
5115 let inner = std::sync::Arc::new(InnerFilterCap);
5116 let outer = DelegatingFilterCap {
5117 id: "delegating_filter",
5118 inner: inner.clone(),
5119 };
5120
5121 let mut registry = CapabilityRegistry::new();
5122 registry.register(outer);
5123
5124 let configs = vec![AgentCapabilityConfig {
5125 capability_ref: CapabilityId::new("delegating_filter"),
5126 config: serde_json::json!({}),
5127 }];
5128
5129 let collected = collect_message_filters_only(&configs, ®istry);
5132 assert_eq!(
5133 collected.message_filter_providers.len(),
5134 1,
5135 "provider from resolved inner capability must be collected"
5136 );
5137 }
5138
5139 struct DelegatingMvpCap {
5140 id: &'static str,
5141 inner: std::sync::Arc<InnerMvpCap>,
5142 }
5143 struct InnerMvpCap;
5144
5145 impl Capability for InnerMvpCap {
5146 fn id(&self) -> &str {
5147 "inner_mvp"
5148 }
5149 fn name(&self) -> &str {
5150 "Inner MVP"
5151 }
5152 fn description(&self) -> &str {
5153 "inner"
5154 }
5155 fn model_view_provider(
5156 &self,
5157 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5158 struct NoopMvp;
5160 impl crate::capabilities::ModelViewProvider for NoopMvp {
5161 fn apply_model_view(
5162 &self,
5163 messages: Vec<Message>,
5164 _config: &serde_json::Value,
5165 _context: &ModelViewContext<'_>,
5166 ) -> Vec<Message> {
5167 messages
5168 }
5169 }
5170 Some(std::sync::Arc::new(NoopMvp))
5171 }
5172 }
5173 impl Capability for DelegatingMvpCap {
5174 fn id(&self) -> &str {
5175 self.id
5176 }
5177 fn name(&self) -> &str {
5178 "Delegating MVP"
5179 }
5180 fn description(&self) -> &str {
5181 "delegating"
5182 }
5183 fn model_view_provider(
5184 &self,
5185 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5186 None }
5188 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5189 Some(&*self.inner)
5190 }
5191 }
5192
5193 #[test]
5194 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
5195 let inner = std::sync::Arc::new(InnerMvpCap);
5196 let outer = DelegatingMvpCap {
5197 id: "delegating_mvp",
5198 inner: inner.clone(),
5199 };
5200
5201 let mut registry = CapabilityRegistry::new();
5202 registry.register(outer);
5203
5204 let configs = vec![AgentCapabilityConfig {
5205 capability_ref: CapabilityId::new("delegating_mvp"),
5206 config: serde_json::json!({}),
5207 }];
5208
5209 let collected = collect_model_view_providers(&configs, ®istry, None);
5212 assert_eq!(
5213 collected.model_view_providers.len(),
5214 1,
5215 "provider from resolved inner capability must be collected"
5216 );
5217 }
5218
5219 #[tokio::test]
5229 async fn test_bashkit_shell_capability_produces_bash_tool() {
5230 let registry = CapabilityRegistry::with_builtins();
5231 let collected =
5232 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5233
5234 let tool_names: Vec<&str> = collected
5235 .tool_definitions
5236 .iter()
5237 .map(|t| t.name())
5238 .collect();
5239 assert!(
5240 tool_names.contains(&"bash"),
5241 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5242 tool_names
5243 );
5244 assert!(
5245 !collected.tools.is_empty(),
5246 "bashkit_shell must provide tool implementations"
5247 );
5248 }
5249
5250 #[tokio::test]
5251 async fn test_generic_harness_capability_set_produces_bash_tool() {
5252 let generic_harness_caps = vec![
5255 "session_file_system".to_string(),
5256 "bashkit_shell".to_string(),
5257 "web_fetch".to_string(),
5258 "session_storage".to_string(),
5259 "session".to_string(),
5260 "agent_instructions".to_string(),
5261 "skills".to_string(),
5262 "infinity_context".to_string(),
5263 "auto_tool_search".to_string(),
5264 ];
5265
5266 let registry = CapabilityRegistry::with_builtins();
5267 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5268
5269 let tool_names: Vec<&str> = collected
5270 .tool_definitions
5271 .iter()
5272 .map(|t| t.name())
5273 .collect();
5274 assert!(
5275 tool_names.contains(&"bash"),
5276 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5277 tool_names
5278 );
5279 }
5280
5281 #[tokio::test]
5282 async fn test_collect_capabilities_tool_count_matches_definitions() {
5283 let registry = CapabilityRegistry::with_builtins();
5286 let collected =
5287 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5288
5289 assert_eq!(
5290 collected.tools.len(),
5291 collected.tool_definitions.len(),
5292 "tool implementations ({}) must match tool definitions ({})",
5293 collected.tools.len(),
5294 collected.tool_definitions.len(),
5295 );
5296 }
5297
5298 #[tokio::test]
5302 async fn test_collect_capabilities_resolves_dependencies() {
5303 let registry = CapabilityRegistry::with_builtins();
5306 let collected =
5307 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5308
5309 assert!(
5311 collected
5312 .applied_ids
5313 .iter()
5314 .any(|id| id == "session_file_system"),
5315 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5316 collected.applied_ids
5317 );
5318
5319 let tool_names: Vec<&str> = collected
5320 .tool_definitions
5321 .iter()
5322 .map(|t| t.name())
5323 .collect();
5324
5325 assert!(
5327 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5328 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5329 tool_names
5330 );
5331
5332 assert_eq!(
5334 collected.tools.len(),
5335 collected.tool_definitions.len(),
5336 "dependency-added tools must have implementations, not just definitions"
5337 );
5338 }
5339
5340 #[test]
5341 fn test_defaults_do_not_include_bash() {
5342 let registry = crate::ToolRegistry::with_defaults();
5345 assert!(
5346 !registry.has("bash"),
5347 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5348 );
5349 }
5350
5351 #[tokio::test]
5358 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5359 let registry = CapabilityRegistry::with_builtins();
5360 let collected =
5361 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5362
5363 let tool_names: Vec<&str> = collected
5364 .tool_definitions
5365 .iter()
5366 .map(|t| t.name())
5367 .collect();
5368 assert!(
5369 tool_names.contains(&"spawn_background"),
5370 "spawn_background must be auto-activated when bashkit_shell (a \
5371 background-capable tool) is in the agent's capability set; got: {:?}",
5372 tool_names
5373 );
5374 assert!(
5375 collected
5376 .applied_ids
5377 .iter()
5378 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5379 "background_execution must be in applied_ids when auto-activated; \
5380 got: {:?}",
5381 collected.applied_ids
5382 );
5383
5384 assert!(
5386 collected
5387 .tools
5388 .iter()
5389 .any(|t| t.name() == "spawn_background"),
5390 "spawn_background tool implementation must be present alongside the \
5391 definition (lockstep contract)"
5392 );
5393 }
5394
5395 #[tokio::test]
5398 async fn test_background_execution_does_not_auto_activate_without_hint() {
5399 let registry = CapabilityRegistry::with_builtins();
5400 let collected =
5402 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5403
5404 let tool_names: Vec<&str> = collected
5405 .tool_definitions
5406 .iter()
5407 .map(|t| t.name())
5408 .collect();
5409 assert!(
5410 !tool_names.contains(&"spawn_background"),
5411 "spawn_background must NOT be activated without a background-capable \
5412 tool; got: {:?}",
5413 tool_names
5414 );
5415 assert!(
5416 !collected
5417 .applied_ids
5418 .iter()
5419 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5420 "background_execution must not appear in applied_ids when no \
5421 background-capable tool is present; got: {:?}",
5422 collected.applied_ids
5423 );
5424 }
5425
5426 #[tokio::test]
5427 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5428 let registry = CapabilityRegistry::with_builtins();
5429 let collected = collect_capabilities(
5430 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5431 ®istry,
5432 &test_ctx(),
5433 )
5434 .await;
5435
5436 assert!(
5437 collected
5438 .tools
5439 .iter()
5440 .any(|tool| tool.name() == "spawn_agent"),
5441 "subagent-only sessions should get the unified spawn_agent adapter"
5442 );
5443 let spawn_agent = collected
5444 .tool_definitions
5445 .iter()
5446 .find(|tool| tool.name() == "spawn_agent")
5447 .expect("spawn_agent definition");
5448 assert_eq!(
5449 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5450 serde_json::json!(["subagent"])
5451 );
5452 assert_eq!(
5453 spawn_agent.concurrency_class(),
5454 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5455 "unified spawn_agent must serialize same-batch spawns before cap checks"
5456 );
5457 }
5458
5459 #[tokio::test]
5460 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5461 let mut registry = CapabilityRegistry::new();
5462 registry.register(AgentHandoffCapability);
5463 let agent_id = crate::typed_id::AgentId::new();
5464 let harness_id = crate::typed_id::HarnessId::new();
5465 let configs = vec![AgentCapabilityConfig {
5466 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5467 config: serde_json::json!({
5468 "targets": [{
5469 "id": "aws_operator",
5470 "name": "AWS Operator",
5471 "agent_id": agent_id,
5472 "harness_id": harness_id
5473 }]
5474 }),
5475 }];
5476 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5477
5478 assert!(
5479 collected
5480 .tools
5481 .iter()
5482 .any(|tool| tool.name() == "spawn_agent"),
5483 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5484 );
5485 let spawn_agent = collected
5486 .tool_definitions
5487 .iter()
5488 .find(|tool| tool.name() == "spawn_agent")
5489 .expect("spawn_agent definition");
5490 assert_eq!(
5491 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5492 serde_json::json!(["agent"])
5493 );
5494 }
5495
5496 #[tokio::test]
5497 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5498 let mut registry = CapabilityRegistry::new();
5499 registry.register(SubagentCapability);
5500 registry.register(AgentHandoffCapability);
5501
5502 let agent_id = crate::typed_id::AgentId::new();
5503 let harness_id = crate::typed_id::HarnessId::new();
5504 let configs = vec![
5505 AgentCapabilityConfig {
5506 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5507 config: serde_json::json!({}),
5508 },
5509 AgentCapabilityConfig {
5510 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5511 config: serde_json::json!({
5512 "targets": [{
5513 "id": "aws_operator",
5514 "name": "AWS Operator",
5515 "agent_id": agent_id,
5516 "harness_id": harness_id
5517 }]
5518 }),
5519 },
5520 ];
5521
5522 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5523 let spawn_agent_defs: Vec<_> = collected
5524 .tool_definitions
5525 .iter()
5526 .filter(|tool| tool.name() == "spawn_agent")
5527 .collect();
5528
5529 assert_eq!(spawn_agent_defs.len(), 1);
5530 let schema = spawn_agent_defs[0].parameters();
5531 assert_eq!(
5532 schema["properties"]["target"]["properties"]["type"]["enum"],
5533 serde_json::json!(["subagent", "agent"])
5534 );
5535 assert!(schema.get("oneOf").is_none());
5538 assert!(schema.get("anyOf").is_none());
5539 assert!(schema.get("allOf").is_none());
5540 assert_eq!(
5541 schema["required"],
5542 serde_json::json!(["name", "instructions", "target"])
5543 );
5544 assert_eq!(
5545 schema["properties"]["target"]["oneOf"],
5546 serde_json::json!([
5547 {
5548 "properties": {"type": {"const": "subagent"}}
5549 },
5550 {
5551 "properties": {"type": {"const": "agent"}},
5552 "required": ["type", "id"]
5553 }
5554 ])
5555 );
5556 }
5557
5558 #[cfg(feature = "a2a")]
5559 #[tokio::test]
5560 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5561 let mut registry = CapabilityRegistry::new();
5562 registry.register(SubagentCapability);
5563 registry.register(A2aAgentDelegationCapability);
5564
5565 let configs = vec![
5566 AgentCapabilityConfig {
5567 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5568 config: serde_json::json!({}),
5569 },
5570 AgentCapabilityConfig {
5571 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5572 config: serde_json::json!({
5573 "agents": [{
5574 "id": "local_app",
5575 "name": "Local App",
5576 "base_url": "https://example.com"
5577 }]
5578 }),
5579 },
5580 ];
5581
5582 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5583 let spawn_agent_defs: Vec<_> = collected
5584 .tool_definitions
5585 .iter()
5586 .filter(|tool| tool.name() == "spawn_agent")
5587 .collect();
5588
5589 assert_eq!(spawn_agent_defs.len(), 1);
5590 assert_eq!(
5591 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5592 serde_json::json!(["subagent", "external_a2a"])
5593 );
5594 assert_eq!(
5595 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5596 serde_json::json!(["background", "foreground"])
5597 );
5598 assert!(
5599 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5600 .as_str()
5601 .expect("mode description")
5602 .contains("wait")
5603 );
5604 let schema = spawn_agent_defs[0].parameters();
5605 assert!(schema.get("oneOf").is_none());
5606 assert_eq!(
5610 schema["required"],
5611 serde_json::json!(["name", "instructions", "target"])
5612 );
5613 assert_eq!(
5614 schema["properties"]["target"]["oneOf"],
5615 serde_json::json!([
5616 {
5617 "properties": {"type": {"const": "subagent"}}
5618 },
5619 {
5620 "properties": {"type": {"const": "external_a2a"}},
5621 "anyOf": [
5622 {"required": ["id"]},
5623 {"required": ["external_agent_id"]}
5624 ]
5625 }
5626 ])
5627 );
5628 }
5629
5630 struct ExistingSpawnAgentCapability;
5631
5632 impl Capability for ExistingSpawnAgentCapability {
5633 fn id(&self) -> &str {
5634 "existing_spawn_agent"
5635 }
5636
5637 fn name(&self) -> &str {
5638 "Existing Spawn Agent"
5639 }
5640
5641 fn description(&self) -> &str {
5642 "Test capability that already owns spawn_agent"
5643 }
5644
5645 fn tools(&self) -> Vec<Box<dyn Tool>> {
5646 vec![Box::new(ExistingSpawnAgentTool)]
5647 }
5648 }
5649
5650 struct ExistingSpawnAgentTool;
5651
5652 #[async_trait]
5653 impl Tool for ExistingSpawnAgentTool {
5654 fn name(&self) -> &str {
5655 "spawn_agent"
5656 }
5657
5658 fn description(&self) -> &str {
5659 "Existing spawn_agent test tool"
5660 }
5661
5662 fn parameters_schema(&self) -> serde_json::Value {
5663 serde_json::json!({
5664 "type": "object",
5665 "properties": {
5666 "target": {
5667 "type": "object",
5668 "properties": {
5669 "type": {"type": "string", "enum": ["external_a2a"]}
5670 },
5671 "required": ["type"]
5672 }
5673 },
5674 "required": ["target"]
5675 })
5676 }
5677
5678 async fn execute(
5679 &self,
5680 _arguments: serde_json::Value,
5681 ) -> crate::tools::ToolExecutionResult {
5682 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5683 }
5684 }
5685
5686 #[tokio::test]
5687 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5688 let mut registry = CapabilityRegistry::new();
5689 registry.register(SubagentCapability);
5690 registry.register(ExistingSpawnAgentCapability);
5691
5692 let collected = collect_capabilities(
5693 &[
5694 SUBAGENTS_CAPABILITY_ID.to_string(),
5695 "existing_spawn_agent".to_string(),
5696 ],
5697 ®istry,
5698 &test_ctx(),
5699 )
5700 .await;
5701
5702 let spawn_agent_defs: Vec<_> = collected
5703 .tool_definitions
5704 .iter()
5705 .filter(|tool| tool.name() == "spawn_agent")
5706 .collect();
5707 assert_eq!(spawn_agent_defs.len(), 1);
5708 assert_eq!(
5709 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5710 serde_json::json!(["external_a2a"])
5711 );
5712 }
5713
5714 #[tokio::test]
5715 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5716 let mut registry = CapabilityRegistry::new();
5717 registry.register(AgentHandoffCapability);
5718 registry.register(ExistingSpawnAgentCapability);
5719
5720 let agent_id = crate::typed_id::AgentId::new();
5721 let harness_id = crate::typed_id::HarnessId::new();
5722 let configs = vec![
5723 AgentCapabilityConfig {
5724 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5725 config: serde_json::json!({
5726 "targets": [{
5727 "id": "aws_operator",
5728 "name": "AWS Operator",
5729 "agent_id": agent_id,
5730 "harness_id": harness_id
5731 }]
5732 }),
5733 },
5734 AgentCapabilityConfig {
5735 capability_ref: CapabilityId::new("existing_spawn_agent"),
5736 config: serde_json::json!({}),
5737 },
5738 ];
5739
5740 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5741
5742 let spawn_agent_defs: Vec<_> = collected
5743 .tool_definitions
5744 .iter()
5745 .filter(|tool| tool.name() == "spawn_agent")
5746 .collect();
5747 assert_eq!(spawn_agent_defs.len(), 1);
5748 assert_eq!(
5749 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5750 serde_json::json!(["external_a2a"])
5751 );
5752 }
5753
5754 #[tokio::test]
5758 async fn test_background_execution_explicit_selection_is_idempotent() {
5759 let registry = CapabilityRegistry::with_builtins();
5760 let collected = collect_capabilities(
5761 &[
5762 "bashkit_shell".to_string(),
5763 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5764 ],
5765 ®istry,
5766 &test_ctx(),
5767 )
5768 .await;
5769
5770 let spawn_background_count = collected
5771 .tool_definitions
5772 .iter()
5773 .filter(|t| t.name() == "spawn_background")
5774 .count();
5775 assert_eq!(
5776 spawn_background_count, 1,
5777 "spawn_background must appear exactly once even when \
5778 background_execution is selected explicitly alongside a \
5779 background-capable tool"
5780 );
5781 let applied_count = collected
5782 .applied_ids
5783 .iter()
5784 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5785 .count();
5786 assert_eq!(
5787 applied_count, 1,
5788 "background_execution must appear exactly once in applied_ids"
5789 );
5790 }
5791
5792 #[test]
5797 fn test_defaults_do_not_include_spawn_background() {
5798 let registry = crate::ToolRegistry::with_defaults();
5799 assert!(
5800 !registry.has("spawn_background"),
5801 "with_defaults() must not include 'spawn_background' — it comes \
5802 from the background_execution capability (EVE-501)"
5803 );
5804 }
5805
5806 #[test]
5811 fn test_capability_features_default_empty() {
5812 let registry = CapabilityRegistry::with_builtins();
5813
5814 let noop = registry.get("noop").unwrap();
5816 assert!(noop.features().is_empty());
5817
5818 let current_time = registry.get("current_time").unwrap();
5819 assert!(current_time.features().is_empty());
5820 }
5821
5822 #[test]
5823 fn test_file_system_capability_features() {
5824 let registry = CapabilityRegistry::with_builtins();
5825
5826 let fs = registry.get("session_file_system").unwrap();
5827 assert_eq!(fs.features(), vec!["file_system"]);
5828 }
5829
5830 #[test]
5831 fn test_bashkit_shell_capability_features() {
5832 let registry = CapabilityRegistry::with_builtins();
5833
5834 let bash = registry.get("bashkit_shell").unwrap();
5835 assert_eq!(bash.features(), vec!["file_system"]);
5836 }
5837
5838 #[test]
5839 fn test_alias_resolves_to_canonical_capability() {
5840 let registry = CapabilityRegistry::with_builtins();
5841
5842 let via_alias = registry.get("virtual_bash").unwrap();
5844 assert_eq!(via_alias.id(), "bashkit_shell");
5845 assert!(registry.has("virtual_bash"));
5846 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5847 assert_eq!(
5848 registry.canonical_id("bashkit_shell"),
5849 Some("bashkit_shell")
5850 );
5851 assert_eq!(registry.canonical_id("nonexistent"), None);
5852 }
5853
5854 #[test]
5855 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5856 let registry = CapabilityRegistry::with_builtins();
5857
5858 let resolved = resolve_dependencies(
5861 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5862 ®istry,
5863 )
5864 .unwrap();
5865 let bash_ids: Vec<_> = resolved
5866 .resolved_ids
5867 .iter()
5868 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5869 .collect();
5870 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5871 assert!(
5873 !resolved
5874 .added_as_dependencies
5875 .contains(&"bashkit_shell".to_string())
5876 );
5877 }
5878
5879 #[test]
5880 fn test_alias_preserves_explicit_config_in_resolution() {
5881 let registry = CapabilityRegistry::with_builtins();
5882
5883 let configs = vec![AgentCapabilityConfig::with_config(
5884 "virtual_bash".to_string(),
5885 serde_json::json!({"key": "value"}),
5886 )];
5887 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5888 let bash = resolved
5889 .iter()
5890 .find(|c| c.capability_id() == "bashkit_shell")
5891 .expect("alias must resolve to canonical bashkit_shell config");
5892 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5893 }
5894
5895 #[test]
5896 fn test_unregister_by_alias_removes_capability_and_aliases() {
5897 let mut registry = CapabilityRegistry::with_builtins();
5898
5899 assert!(registry.unregister("virtual_bash").is_some());
5900 assert!(!registry.has("bashkit_shell"));
5901 assert!(!registry.has("virtual_bash"));
5902 }
5903
5904 #[test]
5905 fn test_session_storage_capability_features() {
5906 let registry = CapabilityRegistry::with_builtins();
5907
5908 let storage = registry.get("session_storage").unwrap();
5909 let features = storage.features();
5910 assert!(features.contains(&"secrets"));
5911 assert!(features.contains(&"key_value"));
5912 }
5913
5914 #[test]
5915 fn test_session_schedule_capability_features() {
5916 let registry = CapabilityRegistry::with_builtins();
5917
5918 let schedule = registry.get("session_schedule").unwrap();
5919 assert_eq!(schedule.features(), vec!["schedules"]);
5920 }
5921
5922 #[test]
5923 fn test_session_sql_database_capability_features() {
5924 let registry = CapabilityRegistry::with_builtins();
5925
5926 let sql = registry.get("session_sql_database").unwrap();
5927 assert_eq!(sql.features(), vec!["sql_database"]);
5928 }
5929
5930 #[test]
5931 fn test_sample_data_capability_features() {
5932 let registry = CapabilityRegistry::with_builtins();
5933
5934 let sample = registry.get("sample_data").unwrap();
5935 assert_eq!(sample.features(), vec!["file_system"]);
5936 }
5937
5938 #[test]
5939 fn test_compute_features_empty() {
5940 let registry = CapabilityRegistry::with_builtins();
5941
5942 let features = compute_features(&[], ®istry);
5943 assert!(features.is_empty());
5944 }
5945
5946 #[test]
5947 fn test_compute_features_single_capability() {
5948 let registry = CapabilityRegistry::with_builtins();
5949
5950 let features = compute_features(&["session_schedule".to_string()], ®istry);
5951 assert_eq!(features, vec!["schedules"]);
5952 }
5953
5954 #[test]
5955 fn test_compute_features_multiple_capabilities() {
5956 let registry = CapabilityRegistry::with_builtins();
5957
5958 let features = compute_features(
5959 &[
5960 "session_file_system".to_string(),
5961 "session_storage".to_string(),
5962 "session_schedule".to_string(),
5963 ],
5964 ®istry,
5965 );
5966 assert!(features.contains(&"file_system".to_string()));
5967 assert!(features.contains(&"secrets".to_string()));
5968 assert!(features.contains(&"key_value".to_string()));
5969 assert!(features.contains(&"schedules".to_string()));
5970 }
5971
5972 #[test]
5973 fn test_compute_features_deduplicates() {
5974 let registry = CapabilityRegistry::with_builtins();
5975
5976 let features = compute_features(
5978 &[
5979 "session_file_system".to_string(),
5980 "bashkit_shell".to_string(),
5981 ],
5982 ®istry,
5983 );
5984 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5985 assert_eq!(file_system_count, 1, "file_system should appear only once");
5986 }
5987
5988 #[test]
5989 fn test_compute_features_includes_dependency_features() {
5990 let registry = CapabilityRegistry::with_builtins();
5991
5992 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5994 assert!(features.contains(&"file_system".to_string()));
5995 }
5996
5997 #[test]
5998 fn test_compute_features_generic_harness_set() {
5999 let registry = CapabilityRegistry::with_builtins();
6000
6001 let features = compute_features(
6003 &[
6004 "session_file_system".to_string(),
6005 "bashkit_shell".to_string(),
6006 "session_storage".to_string(),
6007 "session".to_string(),
6008 "session_schedule".to_string(),
6009 ],
6010 ®istry,
6011 );
6012 assert!(features.contains(&"file_system".to_string()));
6013 assert!(features.contains(&"secrets".to_string()));
6014 assert!(features.contains(&"key_value".to_string()));
6015 assert!(features.contains(&"schedules".to_string()));
6016 }
6017
6018 #[test]
6019 fn test_compute_features_unknown_capability_ignored() {
6020 let registry = CapabilityRegistry::with_builtins();
6021
6022 let features = compute_features(
6023 &["unknown_cap".to_string(), "session_schedule".to_string()],
6024 ®istry,
6025 );
6026 assert_eq!(features, vec!["schedules"]);
6027 }
6028
6029 #[test]
6030 fn test_risk_level_ordering() {
6031 assert!(RiskLevel::Low < RiskLevel::Medium);
6032 assert!(RiskLevel::Medium < RiskLevel::High);
6033 }
6034
6035 #[test]
6036 fn test_risk_level_serde_roundtrip() {
6037 let high = RiskLevel::High;
6038 let json = serde_json::to_string(&high).unwrap();
6039 assert_eq!(json, "\"high\"");
6040 let back: RiskLevel = serde_json::from_str(&json).unwrap();
6041 assert_eq!(back, RiskLevel::High);
6042 }
6043
6044 #[test]
6045 fn test_capability_risk_levels() {
6046 let registry = CapabilityRegistry::with_builtins();
6047
6048 let bash = registry.get("bashkit_shell").unwrap();
6050 assert_eq!(bash.risk_level(), RiskLevel::High);
6051
6052 let fetch = registry.get("web_fetch").unwrap();
6054 assert_eq!(fetch.risk_level(), RiskLevel::High);
6055
6056 let noop = registry.get("noop").unwrap();
6058 assert_eq!(noop.risk_level(), RiskLevel::Low);
6059 }
6060
6061 #[tokio::test]
6066 async fn test_apply_capabilities_openai_tool_search() {
6067 let registry = CapabilityRegistry::with_builtins();
6068 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6069
6070 let applied = apply_capabilities(
6071 base_runtime_agent.clone(),
6072 &["openai_tool_search".to_string()],
6073 ®istry,
6074 &test_ctx(),
6075 )
6076 .await;
6077
6078 assert_eq!(
6080 applied.runtime_agent.system_prompt,
6081 base_runtime_agent.system_prompt
6082 );
6083 assert!(applied.tool_registry.is_empty());
6084 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
6085
6086 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6088 assert!(ts.enabled);
6089 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6090 }
6091
6092 #[tokio::test]
6093 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
6094 let registry = CapabilityRegistry::with_builtins();
6095 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6096
6097 let applied = apply_capabilities(
6098 base_runtime_agent,
6099 &[
6100 "current_time".to_string(),
6101 "openai_tool_search".to_string(),
6102 "test_math".to_string(),
6103 ],
6104 ®istry,
6105 &test_ctx(),
6106 )
6107 .await;
6108
6109 assert!(applied.tool_registry.has("get_current_time"));
6111 assert!(applied.tool_registry.has("add"));
6112 assert!(applied.tool_registry.has("subtract"));
6113 assert!(applied.tool_registry.has("multiply"));
6114 assert!(applied.tool_registry.has("divide"));
6115
6116 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6118 assert!(ts.enabled);
6119 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6120 }
6121
6122 #[tokio::test]
6123 async fn test_collect_capabilities_tool_search_custom_threshold() {
6124 let registry = CapabilityRegistry::with_builtins();
6125
6126 let configs = vec![AgentCapabilityConfig {
6127 capability_ref: CapabilityId::new("openai_tool_search"),
6128 config: serde_json::json!({"threshold": 5}),
6129 }];
6130
6131 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6132
6133 let ts = collected.tool_search.as_ref().unwrap();
6134 assert!(ts.enabled);
6135 assert_eq!(ts.threshold, 5);
6136 }
6137
6138 #[tokio::test]
6139 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
6140 let registry = CapabilityRegistry::with_builtins();
6141
6142 let configs = vec![
6143 AgentCapabilityConfig {
6144 capability_ref: CapabilityId::new("auto_tool_search"),
6145 config: serde_json::json!({"threshold": 2}),
6146 },
6147 AgentCapabilityConfig {
6148 capability_ref: CapabilityId::new("test_math"),
6149 config: serde_json::json!({}),
6150 },
6151 ];
6152
6153 let ctx = test_ctx().with_model("claude-3-5-haiku");
6157 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6158
6159 assert!(
6160 collected.tool_search.is_none(),
6161 "auto_tool_search must not set a hosted config on a non-native model"
6162 );
6163 assert!(
6164 collected
6165 .tools
6166 .iter()
6167 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6168 "auto_tool_search must contribute the client-side tool_search tool"
6169 );
6170 assert!(
6171 !collected.tool_definition_hooks.is_empty(),
6172 "auto_tool_search must contribute a client-side deferral hook"
6173 );
6174
6175 let mut transformed = collected.tool_definitions.clone();
6176 for hook in &collected.tool_definition_hooks {
6177 transformed = hook.transform(transformed);
6178 }
6179 let add_tool = transformed
6180 .iter()
6181 .find(|tool| tool.name() == "add")
6182 .expect("test_math contributes add");
6183 assert!(
6184 add_tool.parameters().get("properties").is_none(),
6185 "generic auto_tool_search must honor the configured threshold"
6186 );
6187 }
6188
6189 #[tokio::test]
6190 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
6191 let registry = CapabilityRegistry::with_builtins();
6192
6193 let configs = vec![AgentCapabilityConfig {
6194 capability_ref: CapabilityId::new("auto_tool_search"),
6195 config: serde_json::json!({"threshold": 7}),
6196 }];
6197
6198 let ctx = test_ctx().with_model("gpt-5.4");
6201 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6202
6203 let ts = collected
6204 .tool_search
6205 .as_ref()
6206 .expect("auto_tool_search must set a hosted config on a native model");
6207 assert!(ts.enabled);
6208 assert_eq!(ts.threshold, 7);
6209 assert!(
6210 !collected
6211 .tools
6212 .iter()
6213 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6214 "hosted mechanism must not contribute the client-side tool_search tool"
6215 );
6216 assert!(
6217 collected.tool_definition_hooks.is_empty(),
6218 "hosted mechanism must not contribute a client-side deferral hook"
6219 );
6220 }
6221
6222 #[tokio::test]
6223 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6224 let registry = CapabilityRegistry::with_builtins();
6225
6226 let configs = vec![AgentCapabilityConfig {
6227 capability_ref: CapabilityId::new("auto_tool_search"),
6228 config: serde_json::json!({"threshold": 9}),
6229 }];
6230
6231 let ctx = test_ctx().with_model("claude-opus-4-8");
6234 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6235
6236 let ts = collected
6237 .tool_search
6238 .as_ref()
6239 .expect("auto_tool_search must set a hosted config on a native Claude model");
6240 assert!(ts.enabled);
6241 assert_eq!(ts.threshold, 9);
6242 assert!(
6243 !collected
6244 .tools
6245 .iter()
6246 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6247 "hosted mechanism must not contribute the client-side tool_search tool"
6248 );
6249 assert!(
6250 collected.tool_definition_hooks.is_empty(),
6251 "hosted mechanism must not contribute a client-side deferral hook"
6252 );
6253 }
6254
6255 #[tokio::test]
6256 async fn test_collect_capabilities_no_tool_search_without_capability() {
6257 let registry = CapabilityRegistry::with_builtins();
6258
6259 let configs = vec![AgentCapabilityConfig {
6260 capability_ref: CapabilityId::new("current_time"),
6261 config: serde_json::json!({}),
6262 }];
6263
6264 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6265
6266 assert!(collected.tool_search.is_none());
6267 }
6268
6269 #[tokio::test]
6270 async fn test_collect_capabilities_tool_search_category_propagation() {
6271 let registry = CapabilityRegistry::with_builtins();
6272
6273 let configs = vec![
6275 AgentCapabilityConfig {
6276 capability_ref: CapabilityId::new("test_math"),
6277 config: serde_json::json!({}),
6278 },
6279 AgentCapabilityConfig {
6280 capability_ref: CapabilityId::new("openai_tool_search"),
6281 config: serde_json::json!({}),
6282 },
6283 ];
6284
6285 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6286
6287 assert!(collected.tool_search.is_some());
6289
6290 for tool_def in &collected.tool_definitions {
6292 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6294 assert!(
6295 tool_def.category().is_some(),
6296 "Tool {} should have a category from its capability",
6297 tool_def.name()
6298 );
6299 }
6300 }
6301 }
6302
6303 #[tokio::test]
6304 async fn test_apply_capabilities_prompt_caching() {
6305 let registry = CapabilityRegistry::with_builtins();
6306 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6307
6308 let applied = apply_capabilities(
6309 base_runtime_agent.clone(),
6310 &["prompt_caching".to_string()],
6311 ®istry,
6312 &test_ctx(),
6313 )
6314 .await;
6315
6316 assert_eq!(
6317 applied.runtime_agent.system_prompt,
6318 base_runtime_agent.system_prompt
6319 );
6320 assert!(applied.tool_registry.is_empty());
6321 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6322
6323 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6324 assert!(prompt_cache.enabled);
6325 assert_eq!(
6326 prompt_cache.strategy,
6327 crate::driver_registry::PromptCacheStrategy::Auto
6328 );
6329 assert!(prompt_cache.gemini_cached_content.is_none());
6330 }
6331
6332 #[tokio::test]
6333 async fn test_apply_capabilities_openrouter_server_tools() {
6334 let registry = CapabilityRegistry::with_builtins();
6335 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6336
6337 let configs = vec![AgentCapabilityConfig {
6338 capability_ref: CapabilityId::new("openrouter_server_tools"),
6339 config: serde_json::json!({
6340 "tools": ["web_search", "datetime"],
6341 "web_search_max_results": 4,
6342 }),
6343 }];
6344
6345 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6346 let routing = collected
6347 .openrouter_routing
6348 .as_ref()
6349 .expect("server tools produce routing config");
6350 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6351 assert_eq!(
6352 kinds,
6353 vec![
6354 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6355 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6356 ]
6357 );
6358
6359 let applied = apply_capabilities(
6362 base_runtime_agent,
6363 &["openrouter_server_tools".to_string()],
6364 ®istry,
6365 &test_ctx(),
6366 )
6367 .await;
6368 assert!(applied.tool_registry.is_empty());
6369 assert!(applied.runtime_agent.openrouter_routing.is_none());
6370 }
6371
6372 #[tokio::test]
6373 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6374 let registry = CapabilityRegistry::with_builtins();
6375
6376 let configs = vec![AgentCapabilityConfig {
6377 capability_ref: CapabilityId::new("prompt_caching"),
6378 config: serde_json::json!({"strategy": "auto"}),
6379 }];
6380
6381 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6382
6383 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6384 assert!(prompt_cache.enabled);
6385 assert_eq!(
6386 prompt_cache.strategy,
6387 crate::driver_registry::PromptCacheStrategy::Auto
6388 );
6389 assert!(prompt_cache.gemini_cached_content.is_none());
6390 }
6391
6392 #[tokio::test]
6393 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6394 let registry = CapabilityRegistry::with_builtins();
6395
6396 let configs = vec![AgentCapabilityConfig {
6397 capability_ref: CapabilityId::new("prompt_caching"),
6398 config: serde_json::json!({
6399 "strategy": "auto",
6400 "gemini_cached_content": "cachedContents/demo-cache"
6401 }),
6402 }];
6403
6404 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6405
6406 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6407 assert_eq!(
6408 prompt_cache.gemini_cached_content.as_deref(),
6409 Some("cachedContents/demo-cache")
6410 );
6411 }
6412
6413 #[tokio::test]
6414 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6415 let registry = CapabilityRegistry::with_builtins();
6416
6417 let collected = collect_capabilities_with_configs(
6419 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6420 ®istry,
6421 &test_ctx(),
6422 )
6423 .await;
6424 assert_eq!(collected.parallel_tool_calls, Some(true));
6425
6426 let collected = collect_capabilities_with_configs(
6428 &[AgentCapabilityConfig {
6429 capability_ref: CapabilityId::new("parallel_tool_calls"),
6430 config: serde_json::json!({"mode": "avoid"}),
6431 }],
6432 ®istry,
6433 &test_ctx(),
6434 )
6435 .await;
6436 assert_eq!(collected.parallel_tool_calls, Some(false));
6437
6438 let collected = collect_capabilities_with_configs(
6440 &[AgentCapabilityConfig {
6441 capability_ref: CapabilityId::new("parallel_tool_calls"),
6442 config: serde_json::json!({"mode": "none"}),
6443 }],
6444 ®istry,
6445 &test_ctx(),
6446 )
6447 .await;
6448 assert_eq!(collected.parallel_tool_calls, None);
6449
6450 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6452 assert_eq!(collected.parallel_tool_calls, None);
6453 }
6454
6455 #[tokio::test]
6456 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6457 let registry = CapabilityRegistry::with_builtins();
6458
6459 let applied = apply_capabilities(
6461 RuntimeAgent::new("p", "gpt-5.2"),
6462 &["parallel_tool_calls".to_string()],
6463 ®istry,
6464 &test_ctx(),
6465 )
6466 .await;
6467 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6468
6469 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6471 base.parallel_tool_calls = Some(false);
6472 let applied = apply_capabilities(
6473 base,
6474 &["parallel_tool_calls".to_string()],
6475 ®istry,
6476 &test_ctx(),
6477 )
6478 .await;
6479 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6480 }
6481
6482 struct SkillContributingCapability;
6487
6488 impl Capability for SkillContributingCapability {
6489 fn id(&self) -> &str {
6490 "contributes_skills"
6491 }
6492 fn name(&self) -> &str {
6493 "Contributes Skills"
6494 }
6495 fn description(&self) -> &str {
6496 "Test capability that contributes skills."
6497 }
6498 fn contribute_skills(&self) -> Vec<SkillContribution> {
6499 vec![
6500 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6501 .with_files(vec![(
6502 "scripts/a.sh".to_string(),
6503 "#!/bin/sh\necho a\n".to_string(),
6504 )]),
6505 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6506 .with_user_invocable(false),
6507 ]
6508 }
6509 }
6510
6511 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6512 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6513 MountSource::InlineFile { content, .. } => content.as_str(),
6514 _ => panic!("Expected InlineFile for SKILL.md"),
6515 }
6516 }
6517
6518 #[tokio::test]
6519 async fn test_contribute_skills_normalized_to_mounts() {
6520 let mut registry = CapabilityRegistry::new();
6521 registry.register(SkillContributingCapability);
6522
6523 let configs = vec![AgentCapabilityConfig {
6524 capability_ref: CapabilityId::new("contributes_skills"),
6525 config: serde_json::json!({}),
6526 }];
6527
6528 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6529
6530 let skill_mounts: Vec<_> = collected
6531 .mounts
6532 .iter()
6533 .filter(|m| m.path.starts_with("/.agents/skills/"))
6534 .collect();
6535 assert_eq!(skill_mounts.len(), 2);
6536
6537 for m in &skill_mounts {
6540 assert!(m.is_readonly());
6541 assert_eq!(m.capability_id, "contributes_skills");
6542 }
6543
6544 let alpha = skill_mounts
6545 .iter()
6546 .find(|m| m.path == "/.agents/skills/alpha-skill")
6547 .expect("alpha-skill mount missing");
6548 match &alpha.source {
6549 MountSource::InlineDirectory { entries } => {
6550 assert!(entries.contains_key("SKILL.md"));
6551 assert!(entries.contains_key("scripts/a.sh"));
6552 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6553 assert_eq!(parsed.name, "alpha-skill");
6554 assert!(parsed.user_invocable);
6555 }
6556 _ => panic!("Expected InlineDirectory"),
6557 }
6558
6559 let beta = skill_mounts
6560 .iter()
6561 .find(|m| m.path == "/.agents/skills/beta-skill")
6562 .expect("beta-skill mount missing");
6563 match &beta.source {
6564 MountSource::InlineDirectory { entries } => {
6565 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6566 assert!(!parsed.user_invocable);
6567 }
6568 _ => panic!("Expected InlineDirectory"),
6569 }
6570 }
6571
6572 #[tokio::test]
6573 async fn test_contribute_skills_default_empty() {
6574 let mut registry = CapabilityRegistry::new();
6577 registry.register(FilterTestCapability { priority: 0 });
6578
6579 let configs = vec![AgentCapabilityConfig {
6580 capability_ref: CapabilityId::new("filter_test"),
6581 config: serde_json::json!({}),
6582 }];
6583
6584 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6585 assert!(
6586 collected
6587 .mounts
6588 .iter()
6589 .all(|m| !m.path.starts_with("/.agents/skills/"))
6590 );
6591 }
6592
6593 struct LocalizedCapability;
6594
6595 impl Capability for LocalizedCapability {
6596 fn id(&self) -> &str {
6597 "localized"
6598 }
6599 fn name(&self) -> &str {
6600 "Localized"
6601 }
6602 fn description(&self) -> &str {
6603 "English description"
6604 }
6605 fn localizations(&self) -> Vec<CapabilityLocalization> {
6606 vec![
6607 CapabilityLocalization {
6608 locale: "en",
6609 name: None,
6610 description: None,
6611 config_description: Some("Controls things."),
6612 config_overlay: None,
6613 },
6614 CapabilityLocalization {
6615 locale: "uk",
6616 name: Some("Локалізована"),
6617 description: Some("Український опис"),
6618 config_description: Some("Керує налаштуваннями."),
6619 config_overlay: None,
6620 },
6621 ]
6622 }
6623 }
6624
6625 #[test]
6626 fn localized_name_falls_back_exact_language_then_base() {
6627 let cap = LocalizedCapability;
6628 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6630 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6631 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6633 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6635 assert_eq!(cap.localized_name(None), "Localized");
6636 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6637 assert_eq!(cap.localized_description(Some("de")), "English description");
6638 }
6639
6640 #[test]
6641 fn describe_schema_resolves_config_description_per_locale() {
6642 let cap = LocalizedCapability;
6643 assert_eq!(
6644 cap.describe_schema(Some("uk-UA")).as_deref(),
6645 Some("Керує налаштуваннями.")
6646 );
6647 assert_eq!(
6649 cap.describe_schema(Some("pl")).as_deref(),
6650 Some("Controls things.")
6651 );
6652 assert_eq!(
6653 cap.describe_schema(None).as_deref(),
6654 Some("Controls things.")
6655 );
6656 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6658 }
6659}