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 let feature_flags = crate::FeatureFlags::from_env(&grade);
1562 if internal_flags.session_sandbox {
1563 registry.register(SessionSandboxCapability);
1564 }
1565
1566 if internal_flags.lua {
1570 registry.register(LuaCapability);
1571 registry.register(LuaCodeModeCapability);
1574 }
1575 for plugin in inventory::iter::<IntegrationPlugin>() {
1576 if (!plugin.experimental_only || grade.experimental_features_enabled())
1577 && plugin
1578 .feature_flag
1579 .is_none_or(|f| internal_flags.is_enabled(f) || feature_flags.is_enabled(f))
1580 {
1581 registry.register_boxed((plugin.factory)());
1582 }
1583 }
1584
1585 registry
1586 }
1587
1588 pub fn register(&mut self, capability: impl Capability + 'static) {
1590 self.register_arc(Arc::new(capability));
1591 }
1592
1593 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1595 self.register_arc(Arc::from(capability));
1596 }
1597
1598 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1600 let canonical = capability.id().to_string();
1601 for alias in capability.aliases() {
1602 self.aliases.insert(alias.to_string(), canonical.clone());
1603 }
1604 self.capabilities.insert(canonical, capability);
1605 }
1606
1607 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1609 self.capabilities
1610 .get(id)
1611 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1612 }
1613
1614 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1619 if self.capabilities.contains_key(id) {
1620 Some(id)
1621 } else {
1622 self.aliases
1623 .get(id)
1624 .filter(|c| self.capabilities.contains_key(*c))
1625 .map(String::as_str)
1626 }
1627 }
1628
1629 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1631 let canonical = self.canonical_id(id)?.to_string();
1632 let removed = self.capabilities.remove(&canonical);
1633 self.aliases.retain(|_, target| *target != canonical);
1634 removed
1635 }
1636
1637 pub fn has(&self, id: &str) -> bool {
1639 self.get(id).is_some()
1640 }
1641
1642 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1644 self.capabilities.values().collect()
1645 }
1646
1647 pub fn len(&self) -> usize {
1649 self.capabilities.len()
1650 }
1651
1652 pub fn is_empty(&self) -> bool {
1654 self.capabilities.is_empty()
1655 }
1656
1657 pub fn builder() -> CapabilityRegistryBuilder {
1659 CapabilityRegistryBuilder::new()
1660 }
1661
1662 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1666 for cap in self.capabilities.values() {
1667 for bp in cap.agent_blueprints() {
1668 if bp.id == id {
1669 return Some(bp);
1670 }
1671 }
1672 }
1673 None
1674 }
1675
1676 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1680 for (capability_id, cap) in &self.capabilities {
1681 for bp in cap.agent_blueprints() {
1682 if bp.id == id {
1683 return Some((capability_id.clone(), bp));
1684 }
1685 }
1686 }
1687 None
1688 }
1689
1690 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1692 self.capabilities
1693 .values()
1694 .flat_map(|cap| cap.agent_blueprints())
1695 .collect()
1696 }
1697}
1698
1699impl Default for CapabilityRegistry {
1700 fn default() -> Self {
1701 Self::with_builtins()
1702 }
1703}
1704
1705impl std::fmt::Debug for CapabilityRegistry {
1706 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1707 let ids: Vec<_> = self.capabilities.keys().collect();
1708 f.debug_struct("CapabilityRegistry")
1709 .field("capabilities", &ids)
1710 .finish()
1711 }
1712}
1713
1714pub struct CapabilityRegistryBuilder {
1716 registry: CapabilityRegistry,
1717}
1718
1719impl CapabilityRegistryBuilder {
1720 pub fn new() -> Self {
1722 Self {
1723 registry: CapabilityRegistry::new(),
1724 }
1725 }
1726
1727 pub fn with_builtins() -> Self {
1729 Self {
1730 registry: CapabilityRegistry::with_builtins(),
1731 }
1732 }
1733
1734 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1736 self.registry.register(capability);
1737 self
1738 }
1739
1740 pub fn build(self) -> CapabilityRegistry {
1742 self.registry
1743 }
1744}
1745
1746impl Default for CapabilityRegistryBuilder {
1747 fn default() -> Self {
1748 Self::new()
1749 }
1750}
1751
1752pub struct ModelViewContext<'a> {
1758 pub session_id: SessionId,
1759 pub prior_usage: Option<&'a TokenUsage>,
1760}
1761
1762pub trait ModelViewProvider: Send + Sync {
1768 fn apply_model_view(
1769 &self,
1770 messages: Vec<Message>,
1771 config: &serde_json::Value,
1772 context: &ModelViewContext<'_>,
1773 ) -> Vec<Message>;
1774
1775 fn priority(&self) -> i32 {
1776 0
1777 }
1778}
1779
1780pub struct CollectedCapabilities {
1785 pub system_prompt_parts: Vec<String>,
1787 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1789 pub tools: Vec<Box<dyn Tool>>,
1791 pub tool_definitions: Vec<ToolDefinition>,
1793 pub mounts: Vec<MountPoint>,
1795 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1797 pub applied_ids: Vec<String>,
1799 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1801 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1803 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1806 pub parallel_tool_calls: Option<bool>,
1810 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1812 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1814 pub mcp_servers: ScopedMcpServers,
1816 }
1822
1823#[derive(Debug, Clone, PartialEq, Eq)]
1824pub struct SystemPromptAttribution {
1825 pub capability_id: String,
1826 pub content: String,
1827}
1828
1829impl CollectedCapabilities {
1830 pub fn system_prompt_prefix(&self) -> Option<String> {
1833 if self.system_prompt_parts.is_empty() {
1834 None
1835 } else {
1836 Some(self.system_prompt_parts.join("\n\n"))
1837 }
1838 }
1839
1840 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1844 for (provider, config) in &self.message_filter_providers {
1846 provider.apply_filters(query, config);
1847 }
1848 }
1849
1850 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1853 for (provider, config) in &self.message_filter_providers {
1854 provider.post_load(messages, config);
1855 }
1856 }
1857
1858 pub fn has_message_filters(&self) -> bool {
1860 !self.message_filter_providers.is_empty()
1861 }
1862}
1863
1864struct SpawnAgentTargetProvider {
1865 target_type: &'static str,
1866 tool: Box<dyn Tool>,
1867}
1868
1869#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1871#[serde(rename_all = "snake_case")]
1872pub(crate) enum SpawnMode {
1873 Background,
1874 Foreground,
1875}
1876
1877impl SpawnMode {
1878 pub(crate) fn parse(value: &str) -> Option<Self> {
1879 match value {
1880 "background" => Some(Self::Background),
1881 "foreground" => Some(Self::Foreground),
1882 _ => None,
1883 }
1884 }
1885
1886 pub(crate) fn as_str(self) -> &'static str {
1887 match self {
1888 Self::Background => "background",
1889 Self::Foreground => "foreground",
1890 }
1891 }
1892}
1893
1894struct UnifiedSpawnAgentTool {
1895 providers: Vec<SpawnAgentTargetProvider>,
1896}
1897
1898impl UnifiedSpawnAgentTool {
1899 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1900 Self { providers }
1901 }
1902
1903 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1904 self.providers
1905 .iter()
1906 .find(|provider| provider.target_type == target_type)
1907 .map(|provider| provider.tool.as_ref())
1908 }
1909
1910 fn target_types(&self) -> Vec<&'static str> {
1911 ["subagent", "agent", "external_a2a"]
1912 .into_iter()
1913 .filter(|target_type| {
1914 self.providers
1915 .iter()
1916 .any(|provider| provider.target_type == *target_type)
1917 })
1918 .collect()
1919 }
1920
1921 fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
1926 self.target_types()
1927 .into_iter()
1928 .filter_map(|target_type| match target_type {
1929 "subagent" => Some(serde_json::json!({
1930 "properties": {
1931 "type": {"const": "subagent"}
1932 }
1933 })),
1934 "agent" => Some(serde_json::json!({
1935 "properties": {
1936 "type": {"const": "agent"}
1937 },
1938 "required": ["type", "id"]
1939 })),
1940 "external_a2a" => Some(serde_json::json!({
1941 "properties": {
1942 "type": {"const": "external_a2a"}
1943 },
1944 "anyOf": [
1945 {"required": ["id"]},
1946 {"required": ["external_agent_id"]}
1947 ]
1948 })),
1949 _ => None,
1950 })
1951 .collect()
1952 }
1953
1954 }
1964
1965#[async_trait]
1966impl Tool for UnifiedSpawnAgentTool {
1967 fn narrate(
1968 &self,
1969 tool_call: &ToolCall,
1970 phase: crate::tool_narration::ToolNarrationPhase,
1971 locale: Option<&str>,
1972 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1973 ) -> Option<String> {
1974 let target_type = tool_call
1975 .arguments
1976 .get("target")
1977 .and_then(|target| target.get("type"))
1978 .and_then(serde_json::Value::as_str)?;
1979 self.provider_for(target_type)
1980 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1981 }
1982
1983 fn name(&self) -> &str {
1984 "spawn_agent"
1985 }
1986
1987 fn display_name(&self) -> Option<&str> {
1988 Some("Spawn Agent")
1989 }
1990
1991 fn description(&self) -> &str {
1992 "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."
1993 }
1994
1995 fn parameters_schema(&self) -> serde_json::Value {
1996 serde_json::json!({
1997 "type": "object",
1998 "properties": {
1999 "name": {
2000 "type": "string",
2001 "description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
2002 },
2003 "instructions": {
2004 "type": "string",
2005 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
2006 },
2007 "goal": {
2008 "type": "string",
2009 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
2010 },
2011 "lifetime": {
2012 "type": "string",
2013 "enum": ["linked", "detached"],
2014 "default": "linked",
2015 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
2016 },
2017 "seed": {
2018 "type": "string",
2019 "enum": ["fresh", "fork", "workspace"],
2020 "default": "fresh",
2021 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
2022 },
2023 "target": {
2024 "type": "object",
2025 "properties": {
2026 "type": {
2027 "type": "string",
2028 "enum": self.target_types(),
2029 "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."
2030 },
2031 "id": {
2032 "type": "string",
2033 "description": "Configured target id for first-party handoffs or external A2A agents."
2034 },
2035 "external_agent_id": {
2036 "type": "string",
2037 "description": "Configured external A2A agent id."
2038 }
2039 },
2040 "required": ["type"],
2041 "oneOf": self.target_constraint_branches(),
2042 "additionalProperties": false
2043 },
2044 "mode": {
2045 "type": "string",
2046 "enum": ["background", "foreground"],
2047 "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."
2048 },
2049 "blueprint": {
2050 "type": "string",
2051 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
2052 },
2053 "config": {
2054 "type": "object",
2055 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
2056 },
2057 "result_schema": {
2058 "type": "object",
2059 "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."
2060 },
2061 "message_schema": {
2062 "type": "object",
2063 "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."
2064 },
2065 "public_context": {
2066 "type": "object",
2067 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
2068 },
2069 "wait_timeout_secs": {
2070 "type": "integer",
2071 "minimum": 1,
2072 "maximum": 86400,
2073 "description": "External-A2A-only foreground timeout."
2074 },
2075 "wake_on_completion": {
2076 "type": "boolean",
2077 "description": "External-A2A-only control for background completion wake-ups."
2078 }
2079 },
2080 "required": ["name", "instructions", "target"],
2081 "additionalProperties": false
2082 })
2083 }
2084
2085 fn hints(&self) -> crate::tool_types::ToolHints {
2086 let mut hints = crate::tool_types::ToolHints::default()
2087 .with_long_running(true)
2088 .with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
2089 if self.provider_for("external_a2a").is_some() {
2090 hints = hints.with_open_world(true);
2091 }
2092 hints
2093 }
2094
2095 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
2096 ToolExecutionResult::tool_error(
2097 "spawn_agent requires context. This tool must be executed with session context.",
2098 )
2099 }
2100
2101 async fn execute_with_context(
2102 &self,
2103 arguments: serde_json::Value,
2104 context: &ToolContext,
2105 ) -> ToolExecutionResult {
2106 let target_type = match arguments
2107 .get("target")
2108 .and_then(|target| target.get("type"))
2109 .and_then(serde_json::Value::as_str)
2110 {
2111 Some(target_type) => target_type,
2112 None => {
2113 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
2114 }
2115 };
2116
2117 let Some(provider) = self.provider_for(target_type) else {
2118 let supported = self.target_types().join(", ");
2119 return ToolExecutionResult::tool_error(format!(
2120 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
2121 ));
2122 };
2123 if target_type == "external_a2a"
2124 && arguments
2125 .get("lifetime")
2126 .and_then(serde_json::Value::as_str)
2127 .is_some_and(|value| value == "detached")
2128 {
2129 return ToolExecutionResult::tool_error(
2130 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2131 );
2132 }
2133 if target_type == "external_a2a"
2134 && arguments
2135 .get("message_schema")
2136 .is_some_and(|schema| !schema.is_null())
2137 {
2138 return ToolExecutionResult::tool_error(
2139 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2140 );
2141 }
2142
2143 provider.execute_with_context(arguments, context).await
2144 }
2145
2146 fn requires_context(&self) -> bool {
2147 true
2148 }
2149}
2150
2151pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2156 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2157 return base_system_prompt.to_string();
2158 };
2159
2160 if base_system_prompt.is_empty() {
2161 return additions.to_string();
2162 }
2163
2164 if base_system_prompt.contains("<system-prompt>") {
2165 format!("{base_system_prompt}\n\n{additions}")
2166 } else {
2167 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2168 }
2169}
2170
2171pub struct CollectedMessageFilters {
2178 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2180}
2181
2182pub struct CollectedModelViewProviders {
2184 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2186}
2187
2188impl CollectedMessageFilters {
2194 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2196 for (provider, config) in &self.message_filter_providers {
2197 provider.apply_filters(query, config);
2198 }
2199 }
2200
2201 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2203 for (provider, config) in &self.message_filter_providers {
2204 provider.post_load(messages, config);
2205 }
2206 }
2207}
2208
2209impl CollectedModelViewProviders {
2210 pub fn apply_model_view(
2212 &self,
2213 mut messages: Vec<Message>,
2214 context: &ModelViewContext<'_>,
2215 ) -> Vec<Message> {
2216 for (provider, config) in &self.model_view_providers {
2217 messages = provider.apply_model_view(messages, config, context);
2218 }
2219 messages
2220 }
2221}
2222
2223fn compaction_is_enabled(
2229 capability_configs: &[AgentCapabilityConfig],
2230 registry: &CapabilityRegistry,
2231) -> bool {
2232 capability_configs.iter().any(|cap_config| {
2233 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2234 && registry
2235 .get(cap_config.capability_ref.as_str())
2236 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2237 })
2238}
2239
2240fn message_filter_config_for(
2249 cap_id: &str,
2250 base: &serde_json::Value,
2251 compaction_on: bool,
2252) -> serde_json::Value {
2253 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2254 return base.clone();
2255 }
2256 let mut config = base.clone();
2257 match config.as_object_mut() {
2258 Some(map) => {
2259 map.insert(
2260 "compaction_active".to_string(),
2261 serde_json::Value::Bool(true),
2262 );
2263 }
2264 None => {
2265 config = serde_json::json!({ "compaction_active": true });
2266 }
2267 }
2268 config
2269}
2270
2271pub fn collect_message_filters_only(
2277 capability_configs: &[AgentCapabilityConfig],
2278 registry: &CapabilityRegistry,
2279) -> CollectedMessageFilters {
2280 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2281 Vec::new();
2282 let compaction_on = compaction_is_enabled(capability_configs, registry);
2283
2284 for cap_config in capability_configs {
2285 let cap_id = cap_config.capability_ref.as_str();
2286 if let Some(capability) = registry.get(cap_id) {
2287 if capability.status() != CapabilityStatus::Available {
2288 continue;
2289 }
2290 let effective: &dyn Capability = capability
2293 .resolve_for_model(None)
2294 .unwrap_or_else(|| capability.as_ref());
2295 if let Some(provider) = effective.message_filter_provider() {
2296 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2297 message_filter_providers.push((provider, config));
2298 }
2299 }
2300 }
2301
2302 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2303
2304 CollectedMessageFilters {
2305 message_filter_providers,
2306 }
2307}
2308
2309pub fn collect_model_view_providers(
2316 capability_configs: &[AgentCapabilityConfig],
2317 registry: &CapabilityRegistry,
2318 model: Option<&str>,
2319) -> CollectedModelViewProviders {
2320 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2321
2322 for cap_config in capability_configs {
2323 let cap_id = cap_config.capability_ref.as_str();
2324 if let Some(capability) = registry.get(cap_id) {
2325 if capability.status() != CapabilityStatus::Available {
2326 continue;
2327 }
2328 let effective: &dyn Capability = capability
2329 .resolve_for_model(model)
2330 .unwrap_or_else(|| capability.as_ref());
2331 if let Some(provider) = effective.model_view_provider() {
2332 model_view_providers.push((provider, cap_config.config.clone()));
2333 }
2334 }
2335 }
2336
2337 model_view_providers.sort_by_key(|(p, _)| p.priority());
2338
2339 CollectedModelViewProviders {
2340 model_view_providers,
2341 }
2342}
2343
2344pub fn collect_dynamic_facts(
2350 capability_configs: &[AgentCapabilityConfig],
2351 registry: &CapabilityRegistry,
2352 model: Option<&str>,
2353 ctx: &FactsContext,
2354) -> Vec<Fact> {
2355 let mut dynamic = Vec::new();
2356 for cap_config in capability_configs {
2357 let cap_id = cap_config.capability_ref.as_str();
2358 if let Some(capability) = registry.get(cap_id) {
2359 if capability.status() != CapabilityStatus::Available {
2360 continue;
2361 }
2362 let effective: &dyn Capability = capability
2363 .resolve_for_model(model)
2364 .unwrap_or_else(|| capability.as_ref());
2365 for fact in effective.facts(&cap_config.config, ctx) {
2366 if fact.volatility == Volatility::Dynamic {
2367 dynamic.push(fact);
2368 }
2369 }
2370 }
2371 }
2372 dynamic
2373}
2374
2375pub fn collect_capability_mcp_servers(
2376 capability_configs: &[AgentCapabilityConfig],
2377 registry: &CapabilityRegistry,
2378) -> ScopedMcpServers {
2379 let mut servers = ScopedMcpServers::default();
2380
2381 for cap_config in capability_configs {
2382 let cap_id = cap_config.capability_ref.as_str();
2383 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2386 if let Ok(definition) =
2387 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2388 {
2389 if definition.status != CapabilityStatus::Available {
2390 continue;
2391 }
2392 if let Some(contributed) = definition.mcp_servers {
2393 servers = merge_scoped_mcp_servers(&servers, &contributed);
2394 }
2395 }
2396 continue;
2397 }
2398 if let Some(capability) = registry.get(cap_id) {
2399 if capability.status() != CapabilityStatus::Available {
2400 continue;
2401 }
2402 servers = merge_scoped_mcp_servers(
2403 &servers,
2404 &capability.mcp_servers_with_config(&cap_config.config),
2405 );
2406 }
2407 }
2408
2409 servers
2410}
2411
2412pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2419
2420#[derive(Debug, Clone, PartialEq, Eq)]
2422pub enum DependencyError {
2423 CircularDependency {
2425 capability_id: String,
2427 chain: Vec<String>,
2429 },
2430 TooManyCapabilities {
2432 count: usize,
2434 max: usize,
2436 },
2437}
2438
2439impl std::fmt::Display for DependencyError {
2440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2441 match self {
2442 DependencyError::CircularDependency {
2443 capability_id,
2444 chain,
2445 } => {
2446 write!(
2447 f,
2448 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2449 capability_id,
2450 chain.join(" -> "),
2451 capability_id
2452 )
2453 }
2454 DependencyError::TooManyCapabilities { count, max } => {
2455 write!(
2456 f,
2457 "Too many capabilities after resolution: {} (max: {})",
2458 count, max
2459 )
2460 }
2461 }
2462 }
2463}
2464
2465impl std::error::Error for DependencyError {}
2466
2467#[derive(Debug, Clone)]
2469pub struct ResolvedCapabilities {
2470 pub resolved_ids: Vec<String>,
2473 pub added_as_dependencies: Vec<String>,
2475 pub user_selected: Vec<String>,
2477}
2478
2479pub fn resolve_dependencies(
2499 selected_ids: &[String],
2500 registry: &CapabilityRegistry,
2501) -> Result<ResolvedCapabilities, DependencyError> {
2502 use std::collections::HashSet;
2503
2504 let user_selected: HashSet<String> = selected_ids
2506 .iter()
2507 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2508 .collect();
2509 let mut resolved: Vec<String> = Vec::new();
2510 let mut resolved_set: HashSet<String> = HashSet::new();
2511 let mut added_as_dependencies: Vec<String> = Vec::new();
2512
2513 for cap_id in selected_ids {
2515 resolve_single_capability(
2516 cap_id,
2517 registry,
2518 &mut resolved,
2519 &mut resolved_set,
2520 &mut added_as_dependencies,
2521 &user_selected,
2522 &mut Vec::new(), )?;
2524 }
2525
2526 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2528 return Err(DependencyError::TooManyCapabilities {
2529 count: resolved.len(),
2530 max: MAX_RESOLVED_CAPABILITIES,
2531 });
2532 }
2533
2534 Ok(ResolvedCapabilities {
2535 resolved_ids: resolved,
2536 added_as_dependencies,
2537 user_selected: selected_ids.to_vec(),
2538 })
2539}
2540
2541pub fn resolve_capability_configs(
2546 selected_configs: &[AgentCapabilityConfig],
2547 registry: &CapabilityRegistry,
2548) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2549 let mut selected_ids: Vec<String> = Vec::new();
2550 for config in selected_configs {
2551 if (is_declarative_capability(config.capability_id())
2554 || is_plugin_capability(config.capability_id()))
2555 && let Ok(definition) =
2556 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2557 {
2558 selected_ids.extend(definition.dependencies);
2559 }
2560 selected_ids.push(config.capability_id().to_string());
2561 }
2562 let resolved = resolve_dependencies(&selected_ids, registry)?;
2563
2564 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2567 .iter()
2568 .map(|config| {
2569 let id = config.capability_id();
2570 let id = registry.canonical_id(id).unwrap_or(id);
2571 (id.to_string(), config.config.clone())
2572 })
2573 .collect();
2574
2575 Ok(resolved
2576 .resolved_ids
2577 .into_iter()
2578 .map(|capability_id| {
2579 explicit_configs
2580 .get(&capability_id)
2581 .cloned()
2582 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2583 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2584 })
2585 .collect())
2586}
2587
2588fn resolve_single_capability(
2590 cap_id: &str,
2591 registry: &CapabilityRegistry,
2592 resolved: &mut Vec<String>,
2593 resolved_set: &mut std::collections::HashSet<String>,
2594 added_as_dependencies: &mut Vec<String>,
2595 user_selected: &std::collections::HashSet<String>,
2596 visiting: &mut Vec<String>,
2597) -> Result<(), DependencyError> {
2598 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2602
2603 if resolved_set.contains(cap_id) {
2605 return Ok(());
2606 }
2607
2608 if visiting.contains(&cap_id.to_string()) {
2610 return Err(DependencyError::CircularDependency {
2611 capability_id: cap_id.to_string(),
2612 chain: visiting.clone(),
2613 });
2614 }
2615
2616 let capability = match registry.get(cap_id) {
2618 Some(cap) => cap,
2619 None => {
2620 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2624 && !resolved_set.contains(cap_id)
2625 {
2626 resolved.push(cap_id.to_string());
2627 resolved_set.insert(cap_id.to_string());
2628 if !user_selected.contains(cap_id) {
2629 added_as_dependencies.push(cap_id.to_string());
2630 }
2631 }
2632 return Ok(());
2633 }
2634 };
2635
2636 visiting.push(cap_id.to_string());
2638
2639 for dep_id in capability.dependencies() {
2641 resolve_single_capability(
2642 dep_id,
2643 registry,
2644 resolved,
2645 resolved_set,
2646 added_as_dependencies,
2647 user_selected,
2648 visiting,
2649 )?;
2650 }
2651
2652 visiting.pop();
2654
2655 if !resolved_set.contains(cap_id) {
2657 resolved.push(cap_id.to_string());
2658 resolved_set.insert(cap_id.to_string());
2659
2660 if !user_selected.contains(cap_id) {
2662 added_as_dependencies.push(cap_id.to_string());
2663 }
2664 }
2665
2666 Ok(())
2667}
2668
2669pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2674 use std::collections::HashSet;
2675
2676 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2677 Ok(resolved) => resolved.resolved_ids,
2678 Err(_) => capability_ids.to_vec(),
2679 };
2680
2681 let mut seen = HashSet::new();
2682 let mut features = Vec::new();
2683 for cap_id in &resolved_ids {
2684 if let Some(cap) = registry.get(cap_id) {
2685 for feature in cap.features() {
2686 if seen.insert(feature) {
2687 features.push(feature.to_string());
2688 }
2689 }
2690 }
2691 }
2692 features
2693}
2694
2695pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2698 registry
2699 .get(cap_id)
2700 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2701 .unwrap_or_default()
2702}
2703
2704pub async fn collect_capabilities(
2720 capability_ids: &[String],
2721 registry: &CapabilityRegistry,
2722 ctx: &SystemPromptContext,
2723) -> CollectedCapabilities {
2724 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2727 Ok(resolved) => resolved.resolved_ids,
2728 Err(e) => {
2729 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2730 capability_ids.to_vec()
2731 }
2732 };
2733
2734 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2736 .iter()
2737 .map(|id| AgentCapabilityConfig {
2738 capability_ref: CapabilityId::new(id),
2739 config: serde_json::Value::Object(serde_json::Map::new()),
2740 })
2741 .collect();
2742
2743 collect_capabilities_with_configs(&configs, registry, ctx).await
2744}
2745
2746pub async fn collect_capabilities_with_configs(
2757 capability_configs: &[AgentCapabilityConfig],
2758 registry: &CapabilityRegistry,
2759 ctx: &SystemPromptContext,
2760) -> CollectedCapabilities {
2761 let mut system_prompt_parts: Vec<String> = Vec::new();
2762 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2763 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2764 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2765 let mut mounts: Vec<MountPoint> = Vec::new();
2766 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2767 Vec::new();
2768 let mut applied_ids: Vec<String> = Vec::new();
2769 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2770 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2771 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2772 let mut parallel_tool_calls: Option<bool> = None;
2773 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2774 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2775 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2778 let mut mcp_servers = ScopedMcpServers::default();
2779 let mut static_facts: Vec<Fact> = Vec::new();
2783 let mut has_dynamic_facts = false;
2784 let facts_ctx = FactsContext::new(ctx.session_id);
2785 let compaction_on = compaction_is_enabled(capability_configs, registry);
2786 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2787 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2788
2789 for cap_config in capability_configs {
2790 let cap_id = cap_config.capability_ref.as_str();
2791 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2796 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2797 cap_config.config.clone(),
2798 ) {
2799 Ok(definition) => {
2800 if definition.status != CapabilityStatus::Available {
2801 continue;
2802 }
2803
2804 if let Some(prompt) = definition.system_prompt.as_deref() {
2805 let contribution =
2806 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2807 system_prompt_attributions.push(SystemPromptAttribution {
2808 capability_id: cap_id.to_string(),
2809 content: contribution.clone(),
2810 });
2811 system_prompt_parts.push(contribution);
2812 }
2813
2814 mounts.extend(definition.mounts(cap_id));
2815 if let Some(ref servers) = definition.mcp_servers {
2816 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2817 }
2818 for skill in definition.skill_contributions() {
2819 mounts.push(skill.to_mount(cap_id));
2820 }
2821
2822 applied_ids.push(cap_id.to_string());
2823 }
2824 Err(error) => {
2825 tracing::warn!(
2826 capability_id = %cap_id,
2827 error = %error,
2828 "Skipping invalid declarative/plugin capability config"
2829 );
2830 }
2831 }
2832 continue;
2833 }
2834 if let Some(capability) = registry.get(cap_id) {
2835 if capability.status() != CapabilityStatus::Available {
2837 continue;
2838 }
2839
2840 let effective: &dyn Capability =
2852 match capability.resolve_for_model(ctx.model.as_deref()) {
2853 Some(inner) => inner,
2854 None => capability.as_ref(),
2855 };
2856 let effective_id = effective.id();
2857 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2858 agent_handoff_spawn_config = Some(cap_config.config.clone());
2859 }
2860
2861 if let Some(contribution) = effective
2863 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2864 .await
2865 {
2866 system_prompt_attributions.push(SystemPromptAttribution {
2867 capability_id: cap_id.to_string(),
2868 content: contribution.clone(),
2869 });
2870 system_prompt_parts.push(contribution);
2871 }
2872
2873 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2878 match fact.volatility {
2879 Volatility::Static => static_facts.push(fact),
2880 Volatility::Dynamic => has_dynamic_facts = true,
2881 }
2882 }
2883
2884 for tool in effective.tools_with_config(&cap_config.config) {
2886 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2887 spawn_agent_providers.push(SpawnAgentTargetProvider {
2888 target_type: "external_a2a",
2889 tool,
2890 });
2891 } else {
2892 tools.push(tool);
2893 }
2894 }
2895 tool_definition_hooks
2896 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2897 tool_call_hooks.extend(effective.tool_call_hooks());
2898 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2900 let cap_category = effective.category();
2905 for def in effective.tool_definitions() {
2906 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2907 continue;
2908 }
2909 let def = match (def.category(), cap_category) {
2910 (None, Some(cat)) => def.with_category(cat),
2911 _ => def,
2912 }
2913 .with_capability_attribution(cap_id, Some(capability.name()));
2914 tool_definitions.push(def);
2915 }
2916
2917 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2925 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2926 {
2927 let threshold = cap_config
2929 .config
2930 .get("threshold")
2931 .and_then(|v| v.as_u64())
2932 .map(|v| v as usize)
2933 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2934 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2935 enabled: true,
2936 threshold,
2937 });
2938 }
2939
2940 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2941 let strategy = cap_config
2942 .config
2943 .get("strategy")
2944 .and_then(|v| v.as_str())
2945 .map(|value| match value {
2946 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2947 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2948 })
2949 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2950 let gemini_cached_content = cap_config
2951 .config
2952 .get("gemini_cached_content")
2953 .and_then(|v| v.as_str())
2954 .map(str::to_string);
2955 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2956 enabled: true,
2957 strategy,
2958 gemini_cached_content,
2959 });
2960 }
2961
2962 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2963 parallel_tool_calls =
2964 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2965 }
2966
2967 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2968 let server_tools =
2969 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2970 if !server_tools.is_empty() {
2971 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2972 server_tools,
2973 ..Default::default()
2974 });
2975 }
2976 }
2977
2978 mounts.extend(effective.mounts());
2980
2981 mcp_servers = merge_scoped_mcp_servers(
2982 &mcp_servers,
2983 &effective.mcp_servers_with_config(&cap_config.config),
2984 );
2985
2986 for skill in effective.contribute_skills() {
2990 mounts.push(skill.to_mount(cap_id));
2991 }
2992
2993 if let Some(provider) = effective.message_filter_provider() {
2995 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2996 message_filter_providers.push((provider, config));
2997 }
2998
2999 applied_ids.push(cap_id.to_string());
3000 }
3001 }
3002
3003 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
3008 spawn_agent_providers.push(SpawnAgentTargetProvider {
3009 target_type: "subagent",
3010 tool: Box::new(SpawnSubagentAsAgentTool),
3011 });
3012 }
3013 if let Some(config) = agent_handoff_spawn_config.as_ref() {
3014 spawn_agent_providers.push(SpawnAgentTargetProvider {
3015 target_type: "agent",
3016 tool: Box::new(SpawnAgentHandoffTool::new(config)),
3017 });
3018 }
3019 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
3020 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
3021 let def = tool
3022 .to_definition()
3023 .with_category("Orchestration")
3024 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
3025 tools.push(Box::new(tool));
3026 tool_definitions.push(def);
3027 }
3028
3029 if !applied_ids
3041 .iter()
3042 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
3043 && tool_definitions
3044 .iter()
3045 .any(|def| def.hints().supports_background == Some(true))
3046 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
3047 && bg_cap.status() == CapabilityStatus::Available
3048 {
3049 tools.extend(bg_cap.tools());
3050 let cap_category = bg_cap.category();
3051 for def in bg_cap.tool_definitions() {
3052 let def = match (def.category(), cap_category) {
3053 (None, Some(cat)) => def.with_category(cat),
3054 _ => def,
3055 }
3056 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
3057 tool_definitions.push(def);
3058 }
3059 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
3060 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
3061 }
3062
3063 if let Some(block) = facts::render_facts_block(&static_facts) {
3068 system_prompt_attributions.push(SystemPromptAttribution {
3069 capability_id: "facts".to_string(),
3070 content: block.clone(),
3071 });
3072 system_prompt_parts.push(block);
3073 }
3074 if has_dynamic_facts {
3075 system_prompt_attributions.push(SystemPromptAttribution {
3076 capability_id: "facts".to_string(),
3077 content: FACTS_DYNAMIC_NOTE.to_string(),
3078 });
3079 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
3080 }
3081
3082 tool_call_hooks.extend(narration_hooks);
3086
3087 message_filter_providers.sort_by_key(|(p, _)| p.priority());
3089
3090 CollectedCapabilities {
3091 system_prompt_parts,
3092 system_prompt_attributions,
3093 tools,
3094 tool_definitions,
3095 mounts,
3096 message_filter_providers,
3097 applied_ids,
3098 tool_search,
3099 prompt_cache,
3100 openrouter_routing,
3101 parallel_tool_calls,
3102 tool_definition_hooks,
3103 tool_call_hooks,
3104 mcp_servers,
3105 }
3106}
3107
3108pub struct AppliedCapabilities {
3114 pub runtime_agent: RuntimeAgent,
3116 pub tool_registry: ToolRegistry,
3118 pub applied_ids: Vec<String>,
3120}
3121
3122pub async fn apply_capabilities(
3159 base_runtime_agent: RuntimeAgent,
3160 capability_ids: &[String],
3161 registry: &CapabilityRegistry,
3162 ctx: &SystemPromptContext,
3163) -> AppliedCapabilities {
3164 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3165
3166 let final_system_prompt = compose_system_prompt(
3168 &base_runtime_agent.system_prompt,
3169 collected.system_prompt_prefix().as_deref(),
3170 );
3171
3172 let mut tool_registry = ToolRegistry::new();
3174 for tool in collected.tools {
3175 tool_registry.register_boxed(tool);
3176 }
3177
3178 let mut tools = collected.tool_definitions;
3180 for hook in &collected.tool_definition_hooks {
3181 tools = hook.transform(tools);
3182 }
3183
3184 let runtime_agent = RuntimeAgent {
3185 system_prompt: final_system_prompt,
3186 model: base_runtime_agent.model,
3187 tools,
3188 max_iterations: base_runtime_agent.max_iterations,
3189 temperature: base_runtime_agent.temperature,
3190 max_tokens: base_runtime_agent.max_tokens,
3191 tool_search: collected.tool_search,
3192 prompt_cache: collected.prompt_cache,
3193 openrouter_routing: collected.openrouter_routing,
3194 network_access: base_runtime_agent.network_access,
3195 parallel_tool_calls: base_runtime_agent
3198 .parallel_tool_calls
3199 .or(collected.parallel_tool_calls),
3200 };
3201
3202 AppliedCapabilities {
3203 runtime_agent,
3204 tool_registry,
3205 applied_ids: collected.applied_ids,
3206 }
3207}
3208
3209#[cfg(test)]
3214mod tests {
3215 use super::*;
3216 use crate::typed_id::SessionId;
3217 use std::collections::BTreeSet;
3218 use uuid::Uuid;
3219
3220 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3222
3223 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3224 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3225 }
3226
3227 fn test_ctx() -> SystemPromptContext {
3229 SystemPromptContext::without_file_store(SessionId::new())
3230 }
3231
3232 struct HostAnnotatedCapability;
3234
3235 #[async_trait]
3236 impl Capability for HostAnnotatedCapability {
3237 fn id(&self) -> &str {
3238 "host_annotated"
3239 }
3240 fn name(&self) -> &str {
3241 "Host Annotated"
3242 }
3243 fn description(&self) -> &str {
3244 "Test capability with host-owned metadata."
3245 }
3246 fn metadata(&self) -> Option<serde_json::Value> {
3247 Some(serde_json::json!({"icon": "sparkles", "group": "host"}))
3248 }
3249 }
3250
3251 #[test]
3252 fn capability_metadata_is_an_opt_in_host_hatch() {
3253 assert!(NoopCapability.metadata().is_none());
3255
3256 let metadata = HostAnnotatedCapability.metadata().expect("metadata");
3257 assert_eq!(metadata["icon"], "sparkles");
3258 assert_eq!(metadata["group"], "host");
3259 }
3260
3261 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3263 let mut ids = [
3264 "agent_instructions",
3265 "human_intent",
3266 "budgeting",
3267 "self_budget",
3268 "noop",
3269 "current_time",
3270 "research",
3271 "session_file_system",
3272 "session_storage",
3273 "session",
3274 "session_sql_database",
3275 "test_math",
3276 "test_weather",
3277 "stateless_todo_list",
3278 "web_fetch",
3279 "bashkit_shell",
3280 "background_execution",
3281 "session_schedule",
3282 "btw",
3283 "infinity_context",
3284 "compaction",
3285 "memory",
3286 "message_metadata",
3287 "openai_tool_search",
3288 "claude_tool_search",
3289 "tool_search",
3290 "auto_tool_search",
3291 "prompt_caching",
3292 "parallel_tool_calls",
3293 "session_tasks",
3294 "skills",
3295 "subagents",
3296 "system_commands",
3297 "sample_data",
3298 "data_knowledge",
3299 "knowledge_base",
3300 "knowledge_index",
3301 "citation_retrieval",
3302 "citation_verification",
3303 "tool_output_persistence",
3304 "tool_output_distillation",
3305 "fake_warehouse",
3306 "fake_aws",
3307 "fake_crm",
3308 "fake_financial",
3309 "loop_detection",
3310 "progress_guard",
3311 "usage_limit_auto_continue",
3312 "tool_call_repair",
3313 "error_disclosure",
3314 "prompt_canary_guardrail",
3315 "guardrails",
3316 "user_hooks",
3317 "model_scout",
3318 "openrouter_workspace",
3319 "openrouter_server_tools",
3320 ]
3321 .into_iter()
3322 .collect::<BTreeSet<_>>();
3323 if cfg!(feature = "ui-capabilities") {
3324 ids.insert("openui");
3325 ids.insert("a2ui");
3326 }
3327 ids
3328 }
3329
3330 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3332 let mut ids = [
3333 "agent_instructions",
3334 "human_intent",
3335 "budgeting",
3336 "self_budget",
3337 "noop",
3338 "current_time",
3339 "session_file_system",
3340 "session_storage",
3341 "session",
3342 "stateless_todo_list",
3343 "bashkit_shell",
3344 "btw",
3345 "infinity_context",
3346 "compaction",
3347 "message_metadata",
3348 "openai_tool_search",
3349 "claude_tool_search",
3350 "tool_search",
3351 "auto_tool_search",
3352 "prompt_caching",
3353 "parallel_tool_calls",
3354 "skills",
3355 "system_commands",
3356 "tool_output_persistence",
3357 "tool_output_distillation",
3358 "loop_detection",
3359 "progress_guard",
3360 "tool_call_repair",
3361 "error_disclosure",
3362 "prompt_canary_guardrail",
3363 "guardrails",
3364 "user_hooks",
3365 ]
3366 .into_iter()
3367 .collect::<BTreeSet<_>>();
3368 if cfg!(feature = "web-fetch") {
3369 ids.insert("web_fetch");
3370 }
3371 ids
3372 }
3373
3374 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3376 let mut ids = expected_core_builtin_ids();
3377 ids.insert("agent_handoff");
3378 ids.insert("a2a_agent_delegation");
3379 ids
3380 }
3381
3382 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3383 registry.capabilities.keys().map(String::as_str).collect()
3384 }
3385
3386 #[test]
3396 fn test_capability_registry_with_builtins_dev() {
3397 let _lock = lock_env();
3399 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3400 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3401 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3402 assert!(registry.has("agent_handoff"));
3403 assert!(registry.has("a2a_agent_delegation"));
3404 }
3405
3406 #[test]
3407 fn test_capability_registry_with_builtins_prod() {
3408 let _lock = lock_env();
3410 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3411 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3412 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3413 assert!(!registry.has("docker_container"));
3415 assert!(!registry.has("agent_handoff"));
3416 assert!(!registry.has("a2a_agent_delegation"));
3417 }
3418
3419 #[test]
3420 fn test_capability_registry_runtime_builtins() {
3421 let _lock = lock_env();
3422 unsafe { std::env::remove_var("FEATURE_LUA") };
3423 let registry = CapabilityRegistry::runtime_builtins();
3424 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3425 assert!(registry.has("session_file_system"));
3426 #[cfg(feature = "web-fetch")]
3427 assert!(registry.has("web_fetch"));
3428 assert!(registry.has("bashkit_shell"));
3429
3430 for platform_only in [
3431 "model_scout",
3432 "openrouter_workspace",
3433 "openrouter_server_tools",
3434 "session_tasks",
3435 "session_schedule",
3436 "subagents",
3437 "background_execution",
3438 "session_sql_database",
3439 "knowledge_base",
3440 "knowledge_index",
3441 "sample_data",
3442 "data_knowledge",
3443 "fake_aws",
3444 "fake_crm",
3445 "fake_financial",
3446 "fake_warehouse",
3447 "test_math",
3448 "test_weather",
3449 "research",
3450 ] {
3451 assert!(
3452 !registry.has(platform_only),
3453 "`{platform_only}` should not be in the runtime default registry"
3454 );
3455 }
3456 }
3457
3458 #[test]
3459 fn test_agent_delegation_enabled_by_env_in_prod() {
3460 let _lock = lock_env();
3462 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3463 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3464 assert!(registry.has("agent_handoff"));
3465 assert!(registry.has("a2a_agent_delegation"));
3466 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3467 }
3468
3469 #[test]
3470 fn test_agent_delegation_disabled_by_env_in_dev() {
3471 let _lock = lock_env();
3473 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3474 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3475 assert!(!registry.has("agent_handoff"));
3476 assert!(!registry.has("a2a_agent_delegation"));
3477 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3478 }
3479
3480 #[test]
3481 fn test_capability_registry_get() {
3482 let registry = CapabilityRegistry::with_builtins();
3483
3484 let noop = registry.get("noop").unwrap();
3485 assert_eq!(noop.id(), "noop");
3486 assert_eq!(noop.name(), "No-Op");
3487 assert_eq!(noop.status(), CapabilityStatus::Available);
3488 }
3489
3490 #[test]
3498 fn builtin_capabilities_satisfy_registry_invariants() {
3499 let registry = CapabilityRegistry::with_builtins();
3500
3501 for cap in registry.list() {
3502 let id = cap.id();
3503 assert!(!id.is_empty(), "capability has an empty id");
3504 assert!(
3505 !cap.name().trim().is_empty(),
3506 "capability `{id}` has an empty name"
3507 );
3508
3509 assert!(
3512 registry.get(id).is_some(),
3513 "capability `{id}` does not resolve by its own id"
3514 );
3515
3516 for dep in cap.dependencies() {
3520 assert!(
3521 registry.get(dep).is_some(),
3522 "capability `{id}` depends on `{dep}`, which is not registered"
3523 );
3524 }
3525
3526 let mut seen = std::collections::HashSet::new();
3529 for tool in cap.tools() {
3530 let name = tool.name().to_string();
3531 assert!(
3532 !name.is_empty(),
3533 "capability `{id}` exposes a tool with an empty name"
3534 );
3535 assert!(
3536 seen.insert(name.clone()),
3537 "capability `{id}` exposes duplicate tool name `{name}`"
3538 );
3539 }
3540
3541 let mut def_seen = std::collections::HashSet::new();
3544 for def in cap.tool_definitions() {
3545 let name = def.name().to_string();
3546 assert!(
3547 !name.is_empty(),
3548 "capability `{id}` advertises a tool definition with an empty name"
3549 );
3550 assert!(
3551 def_seen.insert(name.clone()),
3552 "capability `{id}` advertises duplicate tool definition name `{name}`"
3553 );
3554 }
3555 }
3556 }
3557
3558 #[test]
3570 fn builtin_tools_have_narration_or_documented_generic_fallback() {
3571 use crate::tool_narration::{ToolNarrationContext, ToolNarrationPhase};
3572 use crate::tool_types::ToolCall;
3573
3574 const GENERIC_NARRATION_ALLOWLIST: &[(&str, &str)] = &[
3577 ("sample_data", "demo capability with fixture mounts"),
3579 (
3580 "data_knowledge",
3581 "demo knowledge scaffold; fixture data only",
3582 ),
3583 ("fake_aws", "demo/eval fixture tools"),
3584 ("fake_crm", "demo/eval fixture tools"),
3585 ("fake_financial", "demo/eval fixture tools"),
3586 ("fake_warehouse", "demo/eval fixture tools"),
3587 ("test_math", "test fixture capability"),
3588 ("test_weather", "test fixture capability"),
3589 (
3594 "platform",
3595 "operator command surface; tool display names are the intended presentation",
3596 ),
3597 (
3598 "platform_management",
3599 "operator admin surface; mutations narrate via narration_noun, reads use display names",
3600 ),
3601 (
3604 "model_scout",
3605 "operator model-routing tools; display-name presentation is adequate",
3606 ),
3607 (
3608 "openrouter_workspace",
3609 "operator OpenRouter inspection tools; display-name presentation is adequate",
3610 ),
3611 (
3614 "lua",
3615 "arbitrary sandboxed code execution; display-name presentation is adequate",
3616 ),
3617 ];
3618
3619 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3622 let ctx = ToolNarrationContext::default();
3623 let mut missing: Vec<String> = Vec::new();
3624
3625 for cap in registry.list() {
3626 let cap_id = cap.id().to_string();
3627 if GENERIC_NARRATION_ALLOWLIST
3628 .iter()
3629 .any(|(id, _)| *id == cap_id)
3630 {
3631 continue;
3632 }
3633
3634 for tool in cap.tools() {
3635 let def = tool.to_definition();
3636 if def.hints().narration_noun.is_some() {
3639 continue;
3640 }
3641
3642 let call = ToolCall {
3643 id: "call_narration_audit".to_string(),
3644 name: tool.name().to_string(),
3645 arguments: serde_json::json!({}),
3646 };
3647 if cap
3650 .narrate(Some(&def), &call, ToolNarrationPhase::Started, None, ctx)
3651 .is_none()
3652 {
3653 missing.push(format!("{cap_id}::{}", tool.name()));
3654 }
3655 }
3656 }
3657
3658 assert!(
3659 missing.is_empty(),
3660 "These built-in tools fall back to raw tool-call presentation. Implement \
3661 `Tool::narrate` (see knowledge/execution/tool-narration.md), set a `narration_noun` hint, \
3662 or add a documented entry to GENERIC_NARRATION_ALLOWLIST: {missing:?}"
3663 );
3664 }
3665
3666 #[test]
3667 fn test_capability_registry_blueprint_with_capability() {
3668 struct BlueprintProviderCapability;
3669
3670 impl Capability for BlueprintProviderCapability {
3671 fn id(&self) -> &str {
3672 "blueprint_provider"
3673 }
3674 fn name(&self) -> &str {
3675 "Blueprint Provider"
3676 }
3677 fn description(&self) -> &str {
3678 "Capability that provides a blueprint for tests"
3679 }
3680 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3681 vec![AgentBlueprint {
3682 id: "test_blueprint",
3683 name: "Test Blueprint",
3684 description: "Blueprint for capability registry tests",
3685 model: BlueprintModel::Inherit,
3686 system_prompt: "Test prompt",
3687 tools: vec![],
3688 max_turns: None,
3689 config_schema: None,
3690 }]
3691 }
3692 }
3693
3694 let mut registry = CapabilityRegistry::new();
3695 registry.register(BlueprintProviderCapability);
3696
3697 let (capability_id, blueprint) = registry
3698 .blueprint_with_capability("test_blueprint")
3699 .expect("blueprint should resolve with capability id");
3700 assert_eq!(capability_id, "blueprint_provider");
3701 assert_eq!(blueprint.id, "test_blueprint");
3702 }
3703
3704 #[test]
3705 fn test_capability_registry_builder() {
3706 let registry = CapabilityRegistry::builder()
3707 .capability(NoopCapability)
3708 .capability(CurrentTimeCapability)
3709 .build();
3710
3711 assert!(registry.has("noop"));
3712 assert!(registry.has("current_time"));
3713 assert_eq!(registry.len(), 2);
3714 }
3715
3716 #[test]
3717 fn test_capability_status() {
3718 let registry = CapabilityRegistry::with_builtins();
3719
3720 let current_time = registry.get("current_time").unwrap();
3721 assert_eq!(current_time.status(), CapabilityStatus::Available);
3722
3723 let research = registry.get("research").unwrap();
3724 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3725 }
3726
3727 #[test]
3728 fn test_capability_icons_and_categories() {
3729 let registry = CapabilityRegistry::with_builtins();
3730
3731 let noop = registry.get("noop").unwrap();
3732 assert_eq!(noop.icon(), Some("circle-off"));
3733 assert_eq!(noop.category(), Some("Testing"));
3734
3735 let current_time = registry.get("current_time").unwrap();
3736 assert_eq!(current_time.icon(), Some("clock"));
3737 assert_eq!(current_time.category(), Some("Core"));
3738 }
3739
3740 #[test]
3741 fn test_system_prompt_preview_default_delegates_to_addition() {
3742 let registry = CapabilityRegistry::with_builtins();
3743
3744 let test_math = registry.get("test_math").unwrap();
3746 assert_eq!(
3747 test_math.system_prompt_preview().as_deref(),
3748 test_math.system_prompt_addition()
3749 );
3750
3751 let current_time = registry.get("current_time").unwrap();
3753 assert!(current_time.system_prompt_preview().is_none());
3754 assert!(current_time.system_prompt_addition().is_none());
3755 }
3756
3757 #[test]
3758 fn test_system_prompt_preview_dynamic_capability() {
3759 let registry = CapabilityRegistry::with_builtins();
3760 let cap = registry.get("agent_instructions").unwrap();
3761
3762 assert!(cap.system_prompt_addition().is_none());
3764 assert!(cap.system_prompt_preview().is_some());
3765 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3766 }
3767
3768 #[tokio::test]
3773 async fn test_apply_capabilities_empty() {
3774 let registry = CapabilityRegistry::with_builtins();
3775 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3776
3777 let applied =
3778 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3779
3780 assert_eq!(
3781 applied.runtime_agent.system_prompt,
3782 base_runtime_agent.system_prompt
3783 );
3784 assert!(applied.tool_registry.is_empty());
3785 assert!(applied.applied_ids.is_empty());
3786 }
3787
3788 #[tokio::test]
3789 async fn test_apply_capabilities_noop() {
3790 let registry = CapabilityRegistry::with_builtins();
3791 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3792
3793 let applied = apply_capabilities(
3794 base_runtime_agent.clone(),
3795 &["noop".to_string()],
3796 ®istry,
3797 &test_ctx(),
3798 )
3799 .await;
3800
3801 assert_eq!(
3803 applied.runtime_agent.system_prompt,
3804 base_runtime_agent.system_prompt
3805 );
3806 assert!(applied.tool_registry.is_empty());
3807 assert_eq!(applied.applied_ids, vec!["noop"]);
3808 }
3809
3810 #[tokio::test]
3811 async fn test_apply_capabilities_current_time() {
3812 let registry = CapabilityRegistry::with_builtins();
3813 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3814
3815 let applied = apply_capabilities(
3816 base_runtime_agent.clone(),
3817 &["current_time".to_string()],
3818 ®istry,
3819 &test_ctx(),
3820 )
3821 .await;
3822
3823 assert!(
3827 applied
3828 .runtime_agent
3829 .system_prompt
3830 .contains(FACTS_DYNAMIC_NOTE),
3831 "current_time should contribute the dynamic-facts note"
3832 );
3833 assert!(
3834 applied
3835 .runtime_agent
3836 .system_prompt
3837 .contains(&base_runtime_agent.system_prompt),
3838 "base prompt is preserved"
3839 );
3840 assert!(applied.tool_registry.has("get_current_time"));
3841 assert_eq!(applied.tool_registry.len(), 1);
3842 assert_eq!(applied.applied_ids, vec!["current_time"]);
3843 }
3844
3845 #[tokio::test]
3846 async fn test_apply_capabilities_skips_coming_soon() {
3847 let registry = CapabilityRegistry::with_builtins();
3848 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3849
3850 let applied = apply_capabilities(
3852 base_runtime_agent.clone(),
3853 &["research".to_string()],
3854 ®istry,
3855 &test_ctx(),
3856 )
3857 .await;
3858
3859 assert_eq!(
3861 applied.runtime_agent.system_prompt,
3862 base_runtime_agent.system_prompt
3863 );
3864 assert!(applied.applied_ids.is_empty()); }
3866
3867 #[tokio::test]
3868 async fn test_apply_capabilities_multiple() {
3869 let registry = CapabilityRegistry::with_builtins();
3870 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3871
3872 let applied = apply_capabilities(
3873 base_runtime_agent.clone(),
3874 &["noop".to_string(), "current_time".to_string()],
3875 ®istry,
3876 &test_ctx(),
3877 )
3878 .await;
3879
3880 assert!(applied.tool_registry.has("get_current_time"));
3881 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3882 }
3883
3884 #[tokio::test]
3885 async fn test_apply_capabilities_preserves_order() {
3886 let registry = CapabilityRegistry::with_builtins();
3887 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3888
3889 let applied = apply_capabilities(
3891 base_runtime_agent,
3892 &["current_time".to_string(), "noop".to_string()],
3893 ®istry,
3894 &test_ctx(),
3895 )
3896 .await;
3897
3898 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3899 }
3900
3901 #[tokio::test]
3902 async fn test_apply_capabilities_test_math() {
3903 let registry = CapabilityRegistry::with_builtins();
3904 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3905
3906 let applied = apply_capabilities(
3907 base_runtime_agent.clone(),
3908 &["test_math".to_string()],
3909 ®istry,
3910 &test_ctx(),
3911 )
3912 .await;
3913
3914 assert!(
3916 !applied
3917 .runtime_agent
3918 .system_prompt
3919 .contains("<capability id=\"test_math\">")
3920 );
3921 assert!(
3923 applied
3924 .runtime_agent
3925 .system_prompt
3926 .contains("You are a helpful assistant.")
3927 );
3928 assert!(applied.tool_registry.has("add"));
3929 assert!(applied.tool_registry.has("subtract"));
3930 assert!(applied.tool_registry.has("multiply"));
3931 assert!(applied.tool_registry.has("divide"));
3932 assert_eq!(applied.tool_registry.len(), 4);
3933 }
3934
3935 #[tokio::test]
3936 async fn test_apply_capabilities_test_weather() {
3937 let registry = CapabilityRegistry::with_builtins();
3938 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3939
3940 let applied = apply_capabilities(
3941 base_runtime_agent.clone(),
3942 &["test_weather".to_string()],
3943 ®istry,
3944 &test_ctx(),
3945 )
3946 .await;
3947
3948 assert!(
3950 !applied
3951 .runtime_agent
3952 .system_prompt
3953 .contains("<capability id=\"test_weather\">")
3954 );
3955 assert!(applied.tool_registry.has("get_weather"));
3956 assert!(applied.tool_registry.has("get_forecast"));
3957 assert_eq!(applied.tool_registry.len(), 2);
3958 }
3959
3960 #[tokio::test]
3961 async fn test_apply_capabilities_test_math_and_test_weather() {
3962 let registry = CapabilityRegistry::with_builtins();
3963 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3964
3965 let applied = apply_capabilities(
3966 base_runtime_agent.clone(),
3967 &["test_math".to_string(), "test_weather".to_string()],
3968 ®istry,
3969 &test_ctx(),
3970 )
3971 .await;
3972
3973 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3976 assert!(applied.tool_registry.has("get_weather"));
3977 }
3978
3979 #[tokio::test]
3980 async fn test_apply_capabilities_stateless_todo_list() {
3981 let registry = CapabilityRegistry::with_builtins();
3982 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3983
3984 let applied = apply_capabilities(
3985 base_runtime_agent.clone(),
3986 &["stateless_todo_list".to_string()],
3987 ®istry,
3988 &test_ctx(),
3989 )
3990 .await;
3991
3992 assert!(
3994 applied
3995 .runtime_agent
3996 .system_prompt
3997 .contains("Task Management")
3998 );
3999 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
4000 assert!(applied.tool_registry.has("write_todos"));
4001 assert_eq!(applied.tool_registry.len(), 1);
4002 }
4003
4004 #[tokio::test]
4005 async fn test_apply_capabilities_web_fetch() {
4006 let registry = CapabilityRegistry::with_builtins();
4007 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
4008
4009 let applied = apply_capabilities(
4010 base_runtime_agent.clone(),
4011 &["web_fetch".to_string()],
4012 ®istry,
4013 &test_ctx(),
4014 )
4015 .await;
4016
4017 assert!(
4019 applied
4020 .runtime_agent
4021 .system_prompt
4022 .contains(&base_runtime_agent.system_prompt)
4023 );
4024 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
4025 assert!(applied.tool_registry.has("web_fetch"));
4026 assert_eq!(applied.tool_registry.len(), 1);
4027 }
4028
4029 #[tokio::test]
4034 async fn test_xml_tags_wrap_capability_prompts() {
4035 let registry = CapabilityRegistry::with_builtins();
4036 let collected =
4037 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
4038 .await;
4039
4040 assert_eq!(collected.system_prompt_parts.len(), 1);
4041 let part = &collected.system_prompt_parts[0];
4042 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
4043 assert!(part.ends_with("</capability>"));
4044 assert!(part.contains("Task Management"));
4045 }
4046
4047 #[tokio::test]
4048 async fn test_xml_tags_multiple_capabilities() {
4049 let registry = CapabilityRegistry::with_builtins();
4050 let collected = collect_capabilities(
4051 &[
4052 "stateless_todo_list".to_string(),
4053 "session_schedule".to_string(),
4054 ],
4055 ®istry,
4056 &test_ctx(),
4057 )
4058 .await;
4059
4060 assert_eq!(collected.system_prompt_parts.len(), 2);
4061 assert!(
4062 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
4063 );
4064 assert!(
4065 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
4066 );
4067
4068 let prefix = collected.system_prompt_prefix().unwrap();
4069 assert!(prefix.contains("</capability>\n\n<capability"));
4071 }
4072
4073 #[tokio::test]
4074 async fn test_xml_tags_system_prompt_wrapping() {
4075 let registry = CapabilityRegistry::with_builtins();
4076 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4077
4078 let applied = apply_capabilities(
4079 base,
4080 &["stateless_todo_list".to_string()],
4081 ®istry,
4082 &test_ctx(),
4083 )
4084 .await;
4085
4086 let prompt = &applied.runtime_agent.system_prompt;
4087 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4088 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
4090 assert!(prompt.contains("</capability>"));
4091 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4093 }
4094
4095 #[tokio::test]
4096 async fn test_no_xml_wrapping_without_capabilities() {
4097 let registry = CapabilityRegistry::with_builtins();
4098 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4099
4100 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
4101
4102 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4104 assert!(
4105 !applied
4106 .runtime_agent
4107 .system_prompt
4108 .contains("<system-prompt>")
4109 );
4110 }
4111
4112 #[tokio::test]
4113 async fn test_no_xml_wrapping_for_noop_capability() {
4114 let registry = CapabilityRegistry::with_builtins();
4115 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4116
4117 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
4119
4120 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4121 assert!(
4122 !applied
4123 .runtime_agent
4124 .system_prompt
4125 .contains("<system-prompt>")
4126 );
4127 }
4128
4129 #[tokio::test]
4134 async fn test_collect_capabilities_includes_mounts() {
4135 let registry = CapabilityRegistry::with_builtins();
4136
4137 let collected =
4138 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4139
4140 assert!(!collected.mounts.is_empty());
4141 assert_eq!(collected.mounts.len(), 1);
4142 assert_eq!(collected.mounts[0].path, "/samples");
4143 assert!(collected.mounts[0].is_readonly());
4144 }
4145
4146 #[tokio::test]
4147 async fn test_collect_capabilities_empty_mounts_by_default() {
4148 let registry = CapabilityRegistry::with_builtins();
4149
4150 let collected =
4152 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4153
4154 assert!(collected.mounts.is_empty());
4155 }
4156
4157 #[tokio::test]
4158 async fn test_dynamic_facts_add_note_without_static_block() {
4159 let registry = CapabilityRegistry::with_builtins();
4163 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4164 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4165 let prompt = collected.system_prompt_parts.join("\n");
4166 assert!(
4167 prompt.contains(FACTS_DYNAMIC_NOTE),
4168 "dynamic-facts note should be in the cached prompt"
4169 );
4170 assert!(
4171 !prompt.contains("<facts>\n"),
4172 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
4173 );
4174 }
4175
4176 #[tokio::test]
4177 async fn test_static_facts_fold_into_prompt() {
4178 struct StaticFactCap;
4179 impl Capability for StaticFactCap {
4180 fn id(&self) -> &str {
4181 "test_static_fact"
4182 }
4183 fn name(&self) -> &str {
4184 "Static Fact"
4185 }
4186 fn description(&self) -> &str {
4187 "test"
4188 }
4189 fn status(&self) -> CapabilityStatus {
4190 CapabilityStatus::Available
4191 }
4192 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
4193 vec![Fact::stat("workspace_root", "/workspace")]
4194 }
4195 }
4196 let mut registry = CapabilityRegistry::new();
4197 registry.register(StaticFactCap);
4198 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
4199 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4200 let prompt = collected.system_prompt_parts.join("\n");
4201 assert!(
4202 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
4203 "static fact should fold into the cached prompt; got: {prompt}"
4204 );
4205 assert!(
4206 !prompt.contains(FACTS_DYNAMIC_NOTE),
4207 "no dynamic note when only static facts exist"
4208 );
4209 }
4210
4211 #[test]
4212 fn test_collect_dynamic_facts_returns_current_time() {
4213 let registry = CapabilityRegistry::with_builtins();
4214 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4215 let facts = collect_dynamic_facts(
4216 &configs,
4217 ®istry,
4218 None,
4219 &FactsContext::new(SessionId::new()),
4220 );
4221 assert_eq!(facts.len(), 1);
4222 assert_eq!(facts[0].key, "current_time");
4223 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4224 }
4225
4226 #[tokio::test]
4227 async fn test_collect_capabilities_combines_mounts() {
4228 let registry = CapabilityRegistry::with_builtins();
4229
4230 let collected = collect_capabilities(
4233 &["sample_data".to_string(), "current_time".to_string()],
4234 ®istry,
4235 &test_ctx(),
4236 )
4237 .await;
4238
4239 assert_eq!(collected.mounts.len(), 1);
4240 assert!(
4242 collected
4243 .applied_ids
4244 .iter()
4245 .any(|id| id == "session_file_system")
4246 );
4247 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4248 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4249 }
4250
4251 #[test]
4252 fn test_sample_data_capability() {
4253 let registry = CapabilityRegistry::with_builtins();
4254 let cap = registry.get("sample_data").unwrap();
4255
4256 assert_eq!(cap.id(), "sample_data");
4257 assert_eq!(cap.name(), "Sample Data");
4258 assert_eq!(cap.status(), CapabilityStatus::Available);
4259
4260 assert!(cap.system_prompt_addition().is_some());
4262 assert!(cap.tools().is_empty());
4263
4264 assert!(!cap.mounts().is_empty());
4266 }
4267
4268 #[test]
4273 fn test_resolve_dependencies_empty() {
4274 let registry = CapabilityRegistry::with_builtins();
4275
4276 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4277
4278 assert!(resolved.resolved_ids.is_empty());
4279 assert!(resolved.added_as_dependencies.is_empty());
4280 assert!(resolved.user_selected.is_empty());
4281 }
4282
4283 #[test]
4284 fn test_resolve_dependencies_no_deps() {
4285 let registry = CapabilityRegistry::with_builtins();
4286
4287 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4289
4290 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4291 assert!(resolved.added_as_dependencies.is_empty());
4292 }
4293
4294 #[test]
4295 fn test_resolve_dependencies_with_deps() {
4296 let registry = CapabilityRegistry::with_builtins();
4297
4298 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4300
4301 assert_eq!(resolved.resolved_ids.len(), 2);
4303 let fs_pos = resolved
4304 .resolved_ids
4305 .iter()
4306 .position(|id| id == "session_file_system")
4307 .unwrap();
4308 let sd_pos = resolved
4309 .resolved_ids
4310 .iter()
4311 .position(|id| id == "sample_data")
4312 .unwrap();
4313 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4314
4315 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4317 }
4318
4319 #[test]
4320 fn test_resolve_dependencies_already_selected() {
4321 let registry = CapabilityRegistry::with_builtins();
4322
4323 let resolved = resolve_dependencies(
4325 &["session_file_system".to_string(), "sample_data".to_string()],
4326 ®istry,
4327 )
4328 .unwrap();
4329
4330 assert_eq!(resolved.resolved_ids.len(), 2);
4331 assert!(resolved.added_as_dependencies.is_empty());
4333 }
4334
4335 #[test]
4336 fn test_resolve_dependencies_preserves_order() {
4337 let registry = CapabilityRegistry::with_builtins();
4338
4339 let resolved =
4341 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4342 .unwrap();
4343
4344 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4345 }
4346
4347 #[test]
4348 fn test_resolve_dependencies_unknown_capability() {
4349 let registry = CapabilityRegistry::with_builtins();
4350
4351 let resolved =
4353 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4354
4355 assert!(resolved.resolved_ids.is_empty());
4356 }
4357
4358 #[test]
4359 fn test_get_dependencies() {
4360 let registry = CapabilityRegistry::with_builtins();
4361
4362 let deps = get_dependencies("sample_data", ®istry);
4364 assert_eq!(deps, vec!["session_file_system"]);
4365
4366 let deps = get_dependencies("current_time", ®istry);
4368 assert!(deps.is_empty());
4369
4370 let deps = get_dependencies("unknown", ®istry);
4372 assert!(deps.is_empty());
4373 }
4374
4375 #[test]
4376 fn test_sample_data_has_dependency() {
4377 let registry = CapabilityRegistry::with_builtins();
4378 let cap = registry.get("sample_data").unwrap();
4379
4380 let deps = cap.dependencies();
4381 assert_eq!(deps.len(), 1);
4382 assert_eq!(deps[0], "session_file_system");
4383 }
4384
4385 #[test]
4386 fn test_noop_has_no_dependencies() {
4387 let registry = CapabilityRegistry::with_builtins();
4388 let cap = registry.get("noop").unwrap();
4389
4390 assert!(cap.dependencies().is_empty());
4391 }
4392
4393 #[test]
4397 fn test_circular_dependency_error() {
4398 struct CapA;
4400 struct CapB;
4401
4402 impl Capability for CapA {
4403 fn id(&self) -> &str {
4404 "test_cap_a"
4405 }
4406 fn name(&self) -> &str {
4407 "Test A"
4408 }
4409 fn description(&self) -> &str {
4410 "Test capability A"
4411 }
4412 fn dependencies(&self) -> Vec<&'static str> {
4413 vec!["test_cap_b"]
4414 }
4415 }
4416
4417 impl Capability for CapB {
4418 fn id(&self) -> &str {
4419 "test_cap_b"
4420 }
4421 fn name(&self) -> &str {
4422 "Test B"
4423 }
4424 fn description(&self) -> &str {
4425 "Test capability B"
4426 }
4427 fn dependencies(&self) -> Vec<&'static str> {
4428 vec!["test_cap_a"]
4429 }
4430 }
4431
4432 let mut registry = CapabilityRegistry::new();
4433 registry.register(CapA);
4434 registry.register(CapB);
4435
4436 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4437
4438 assert!(result.is_err());
4439 match result.unwrap_err() {
4440 DependencyError::CircularDependency { capability_id, .. } => {
4441 assert_eq!(capability_id, "test_cap_a");
4442 }
4443 _ => panic!("Expected CircularDependency error"),
4444 }
4445 }
4446
4447 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4452
4453 struct FilterTestCapability {
4455 priority: i32,
4456 }
4457
4458 impl Capability for FilterTestCapability {
4459 fn id(&self) -> &str {
4460 "filter_test"
4461 }
4462 fn name(&self) -> &str {
4463 "Filter Test"
4464 }
4465 fn description(&self) -> &str {
4466 "Test capability with message filter"
4467 }
4468 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4469 Some(Arc::new(FilterTestProvider {
4470 priority: self.priority,
4471 }))
4472 }
4473 }
4474
4475 struct FilterTestProvider {
4476 priority: i32,
4477 }
4478
4479 impl MessageFilterProvider for FilterTestProvider {
4480 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4481 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4483 query
4484 .filters
4485 .push(MessageFilter::Search(search.to_string()));
4486 }
4487 }
4488
4489 fn priority(&self) -> i32 {
4490 self.priority
4491 }
4492 }
4493
4494 #[tokio::test]
4495 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4496 let registry = CapabilityRegistry::with_builtins();
4497 let configs = vec![AgentCapabilityConfig {
4498 capability_ref: CapabilityId::new("current_time"),
4499 config: serde_json::json!({}),
4500 }];
4501
4502 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4503
4504 assert!(collected.message_filter_providers.is_empty());
4505 assert!(!collected.has_message_filters());
4506 }
4507
4508 #[tokio::test]
4509 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4510 let mut registry = CapabilityRegistry::new();
4511 registry.register(FilterTestCapability { priority: 0 });
4512
4513 let configs = vec![AgentCapabilityConfig {
4514 capability_ref: CapabilityId::new("filter_test"),
4515 config: serde_json::json!({ "search": "hello" }),
4516 }];
4517
4518 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4519
4520 assert_eq!(collected.message_filter_providers.len(), 1);
4521 assert!(collected.has_message_filters());
4522 }
4523
4524 #[tokio::test]
4525 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4526 struct HighPriorityCapability;
4528 struct LowPriorityCapability;
4529
4530 impl Capability for HighPriorityCapability {
4531 fn id(&self) -> &str {
4532 "high_priority"
4533 }
4534 fn name(&self) -> &str {
4535 "High Priority"
4536 }
4537 fn description(&self) -> &str {
4538 "Test"
4539 }
4540 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4541 Some(Arc::new(FilterTestProvider { priority: 10 }))
4542 }
4543 }
4544
4545 impl Capability for LowPriorityCapability {
4546 fn id(&self) -> &str {
4547 "low_priority"
4548 }
4549 fn name(&self) -> &str {
4550 "Low Priority"
4551 }
4552 fn description(&self) -> &str {
4553 "Test"
4554 }
4555 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4556 Some(Arc::new(FilterTestProvider { priority: -5 }))
4557 }
4558 }
4559
4560 let mut registry = CapabilityRegistry::new();
4561 registry.register(HighPriorityCapability);
4562 registry.register(LowPriorityCapability);
4563
4564 let configs = vec![
4566 AgentCapabilityConfig {
4567 capability_ref: CapabilityId::new("high_priority"),
4568 config: serde_json::json!({}),
4569 },
4570 AgentCapabilityConfig {
4571 capability_ref: CapabilityId::new("low_priority"),
4572 config: serde_json::json!({}),
4573 },
4574 ];
4575
4576 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4577
4578 assert_eq!(collected.message_filter_providers.len(), 2);
4580 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4581 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4582 }
4583
4584 #[tokio::test]
4585 async fn test_collected_capabilities_apply_message_filters() {
4586 let mut registry = CapabilityRegistry::new();
4587 registry.register(FilterTestCapability { priority: 0 });
4588
4589 let configs = vec![AgentCapabilityConfig {
4590 capability_ref: CapabilityId::new("filter_test"),
4591 config: serde_json::json!({ "search": "test_query" }),
4592 }];
4593
4594 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4595
4596 let session_id: SessionId = Uuid::now_v7().into();
4598 let mut query = MessageQuery::new(session_id);
4599
4600 collected.apply_message_filters(&mut query);
4601
4602 assert_eq!(query.filters.len(), 1);
4604 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4605 }
4606
4607 #[tokio::test]
4608 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4609 struct SearchCapability {
4610 id: &'static str,
4611 search_term: &'static str,
4612 priority: i32,
4613 }
4614
4615 struct SearchProvider {
4616 search_term: &'static str,
4617 priority: i32,
4618 }
4619
4620 impl MessageFilterProvider for SearchProvider {
4621 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4622 query
4623 .filters
4624 .push(MessageFilter::Search(self.search_term.to_string()));
4625 }
4626
4627 fn priority(&self) -> i32 {
4628 self.priority
4629 }
4630 }
4631
4632 impl Capability for SearchCapability {
4633 fn id(&self) -> &str {
4634 self.id
4635 }
4636 fn name(&self) -> &str {
4637 "Search"
4638 }
4639 fn description(&self) -> &str {
4640 "Test"
4641 }
4642 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4643 Some(Arc::new(SearchProvider {
4644 search_term: self.search_term,
4645 priority: self.priority,
4646 }))
4647 }
4648 }
4649
4650 let mut registry = CapabilityRegistry::new();
4651 registry.register(SearchCapability {
4652 id: "cap_a",
4653 search_term: "alpha",
4654 priority: 5,
4655 });
4656 registry.register(SearchCapability {
4657 id: "cap_b",
4658 search_term: "beta",
4659 priority: 1,
4660 });
4661 registry.register(SearchCapability {
4662 id: "cap_c",
4663 search_term: "gamma",
4664 priority: 10,
4665 });
4666
4667 let configs = vec![
4668 AgentCapabilityConfig {
4669 capability_ref: CapabilityId::new("cap_a"),
4670 config: serde_json::json!({}),
4671 },
4672 AgentCapabilityConfig {
4673 capability_ref: CapabilityId::new("cap_b"),
4674 config: serde_json::json!({}),
4675 },
4676 AgentCapabilityConfig {
4677 capability_ref: CapabilityId::new("cap_c"),
4678 config: serde_json::json!({}),
4679 },
4680 ];
4681
4682 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4683
4684 let session_id: SessionId = Uuid::now_v7().into();
4685 let mut query = MessageQuery::new(session_id);
4686
4687 collected.apply_message_filters(&mut query);
4688
4689 assert_eq!(query.filters.len(), 3);
4691 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4692 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4693 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4694 }
4695
4696 #[test]
4697 fn test_capability_without_message_filter_returns_none() {
4698 let registry = CapabilityRegistry::with_builtins();
4699
4700 let noop = registry.get("noop").unwrap();
4701 assert!(noop.message_filter_provider().is_none());
4702
4703 let current_time = registry.get("current_time").unwrap();
4704 assert!(current_time.message_filter_provider().is_none());
4705 }
4706
4707 #[tokio::test]
4708 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4709 let mut registry = CapabilityRegistry::new();
4710 registry.register(FilterTestCapability { priority: 0 });
4711
4712 let test_config = serde_json::json!({
4713 "search": "custom_search",
4714 "extra_field": 42
4715 });
4716
4717 let configs = vec![AgentCapabilityConfig {
4718 capability_ref: CapabilityId::new("filter_test"),
4719 config: test_config.clone(),
4720 }];
4721
4722 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4723
4724 assert_eq!(collected.message_filter_providers.len(), 1);
4726 let (_, stored_config) = &collected.message_filter_providers[0];
4727 assert_eq!(*stored_config, test_config);
4728 }
4729
4730 #[test]
4735 fn test_collect_message_filters_only_collects_filters() {
4736 let mut registry = CapabilityRegistry::new();
4737 registry.register(FilterTestCapability { priority: 0 });
4738
4739 let configs = vec![AgentCapabilityConfig {
4740 capability_ref: CapabilityId::new("filter_test"),
4741 config: serde_json::json!({ "search": "test_query" }),
4742 }];
4743
4744 let collected = collect_message_filters_only(&configs, ®istry);
4745
4746 let session_id: SessionId = Uuid::now_v7().into();
4747 let mut query = MessageQuery::new(session_id);
4748 collected.apply_message_filters(&mut query);
4749
4750 assert_eq!(query.filters.len(), 1);
4751 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4752 }
4753
4754 #[test]
4755 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4756 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4757
4758 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4760 assert_eq!(with["compaction_active"], serde_json::json!(true));
4761 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4762
4763 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4764 assert!(without.get("compaction_active").is_none());
4765
4766 let other = message_filter_config_for("other", &base, true);
4768 assert!(other.get("compaction_active").is_none());
4769
4770 let null_base = message_filter_config_for(
4772 INFINITY_CONTEXT_CAPABILITY_ID,
4773 &serde_json::Value::Null,
4774 true,
4775 );
4776 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4777 }
4778
4779 #[test]
4780 fn test_infinity_context_defers_to_compaction_end_to_end() {
4781 use crate::message::Message;
4782
4783 let mut registry = CapabilityRegistry::new();
4784 registry.register(InfinityContextCapability);
4785 registry.register(CompactionCapability);
4786
4787 let tight = serde_json::json!({
4788 "context_budget_tokens": 1,
4789 "min_recent_messages": 1
4790 });
4791
4792 let solo = vec![AgentCapabilityConfig {
4794 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4795 config: tight.clone(),
4796 }];
4797 let mut messages = vec![
4798 Message::user("task"),
4799 Message::assistant("old ".repeat(400)),
4800 Message::user("recent"),
4801 ];
4802 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4803 assert!(
4804 messages
4805 .iter()
4806 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4807 "infinity context alone should trim and notice"
4808 );
4809
4810 let both = vec![
4812 AgentCapabilityConfig {
4813 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4814 config: tight,
4815 },
4816 AgentCapabilityConfig {
4817 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4818 config: serde_json::json!({}),
4819 },
4820 ];
4821 let mut messages = vec![
4822 Message::user("task"),
4823 Message::assistant("old ".repeat(400)),
4824 Message::user("recent"),
4825 ];
4826 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4827 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4828 assert!(
4829 messages
4830 .iter()
4831 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4832 "no hidden-history notice when compaction is the active reducer"
4833 );
4834 }
4835
4836 #[test]
4837 fn test_compaction_is_enabled_detects_compaction() {
4838 let mut registry = CapabilityRegistry::new();
4839 registry.register(CompactionCapability);
4840
4841 let with_compaction = vec![AgentCapabilityConfig {
4842 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4843 config: serde_json::json!({}),
4844 }];
4845 assert!(compaction_is_enabled(&with_compaction, ®istry));
4846
4847 let without = vec![AgentCapabilityConfig {
4848 capability_ref: CapabilityId::new("current_time"),
4849 config: serde_json::json!({}),
4850 }];
4851 assert!(!compaction_is_enabled(&without, ®istry));
4852 }
4853
4854 #[test]
4855 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4856 let registry = CapabilityRegistry::new();
4857
4858 let configs = vec![AgentCapabilityConfig {
4859 capability_ref: CapabilityId::new("nonexistent"),
4860 config: serde_json::json!({}),
4861 }];
4862
4863 let collected = collect_message_filters_only(&configs, ®istry);
4864 assert!(collected.message_filter_providers.is_empty());
4865 }
4866
4867 #[test]
4868 fn test_collect_message_filters_only_preserves_priority_order() {
4869 struct PriorityFilterCap {
4870 id: &'static str,
4871 search_term: &'static str,
4872 priority: i32,
4873 }
4874
4875 struct PriorityFilterProvider {
4876 search_term: &'static str,
4877 priority: i32,
4878 }
4879
4880 impl Capability for PriorityFilterCap {
4881 fn id(&self) -> &str {
4882 self.id
4883 }
4884 fn name(&self) -> &str {
4885 self.id
4886 }
4887 fn description(&self) -> &str {
4888 "priority test"
4889 }
4890 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4891 Some(Arc::new(PriorityFilterProvider {
4892 search_term: self.search_term,
4893 priority: self.priority,
4894 }))
4895 }
4896 }
4897
4898 impl MessageFilterProvider for PriorityFilterProvider {
4899 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4900 query
4901 .filters
4902 .push(MessageFilter::Search(self.search_term.to_string()));
4903 }
4904 fn priority(&self) -> i32 {
4905 self.priority
4906 }
4907 }
4908
4909 let mut registry = CapabilityRegistry::new();
4910 registry.register(PriorityFilterCap {
4911 id: "gamma",
4912 search_term: "gamma",
4913 priority: 10,
4914 });
4915 registry.register(PriorityFilterCap {
4916 id: "alpha",
4917 search_term: "alpha",
4918 priority: 5,
4919 });
4920 registry.register(PriorityFilterCap {
4921 id: "beta",
4922 search_term: "beta",
4923 priority: 1,
4924 });
4925
4926 let configs = vec![
4927 AgentCapabilityConfig {
4928 capability_ref: CapabilityId::new("gamma"),
4929 config: serde_json::json!({}),
4930 },
4931 AgentCapabilityConfig {
4932 capability_ref: CapabilityId::new("alpha"),
4933 config: serde_json::json!({}),
4934 },
4935 AgentCapabilityConfig {
4936 capability_ref: CapabilityId::new("beta"),
4937 config: serde_json::json!({}),
4938 },
4939 ];
4940
4941 let collected = collect_message_filters_only(&configs, ®istry);
4942
4943 let session_id: SessionId = Uuid::now_v7().into();
4944 let mut query = MessageQuery::new(session_id);
4945 collected.apply_message_filters(&mut query);
4946
4947 assert_eq!(query.filters.len(), 3);
4949 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4950 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4951 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4952 }
4953
4954 #[test]
4955 fn test_collect_message_filters_only_post_load_invoked() {
4956 use crate::message::Message;
4957
4958 struct PostLoadCap;
4959 struct PostLoadProvider;
4960
4961 impl Capability for PostLoadCap {
4962 fn id(&self) -> &str {
4963 "post_load_test"
4964 }
4965 fn name(&self) -> &str {
4966 "PostLoad Test"
4967 }
4968 fn description(&self) -> &str {
4969 "test"
4970 }
4971 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4972 Some(Arc::new(PostLoadProvider))
4973 }
4974 }
4975
4976 impl MessageFilterProvider for PostLoadProvider {
4977 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4978 fn priority(&self) -> i32 {
4979 0
4980 }
4981 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4982 messages.reverse();
4984 }
4985 }
4986
4987 let mut registry = CapabilityRegistry::new();
4988 registry.register(PostLoadCap);
4989
4990 let configs = vec![AgentCapabilityConfig {
4991 capability_ref: CapabilityId::new("post_load_test"),
4992 config: serde_json::json!({}),
4993 }];
4994
4995 let collected = collect_message_filters_only(&configs, ®istry);
4996
4997 let mut messages = vec![Message::user("first"), Message::user("second")];
4998 collected.apply_post_load_filters(&mut messages);
4999
5000 assert_eq!(messages[0].text(), Some("second"));
5002 assert_eq!(messages[1].text(), Some("first"));
5003 }
5004
5005 #[test]
5006 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
5007 use crate::tool_types::ToolCall;
5008
5009 fn tool_heavy_messages() -> Vec<Message> {
5010 let mut messages = vec![Message::user("inspect files repeatedly")];
5011 for index in 0..9 {
5012 let call_id = format!("call_{index}");
5013 messages.push(Message::assistant_with_tools(
5014 "",
5015 vec![ToolCall {
5016 id: call_id.clone(),
5017 name: "read_file".to_string(),
5018 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
5019 }],
5020 ));
5021 messages.push(Message::tool_result(
5022 call_id,
5023 Some(serde_json::json!({
5024 "path": "/workspace/src/lib.rs",
5025 "content": format!("{}{}", "large file line\n".repeat(1000), index),
5026 "total_lines": 1000,
5027 "lines_shown": {"start": 1, "end": 1000},
5028 "truncated": false
5029 })),
5030 None,
5031 ));
5032 }
5033 messages
5034 }
5035
5036 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
5037 messages[2]
5038 .tool_result_content()
5039 .and_then(|result| result.result.as_ref())
5040 .and_then(|result| result.get("masked"))
5041 .and_then(|masked| masked.as_bool())
5042 .unwrap_or(false)
5043 }
5044
5045 let mut registry = CapabilityRegistry::new();
5046 registry.register(CompactionCapability);
5047 let context = ModelViewContext {
5048 session_id: SessionId::new(),
5049 prior_usage: None,
5050 };
5051
5052 let no_compaction = collect_model_view_providers(&[], ®istry, None);
5053 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
5054 assert!(!first_tool_result_is_masked(&unmasked));
5055
5056 let compaction = collect_model_view_providers(
5057 &[AgentCapabilityConfig {
5058 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
5059 config: serde_json::json!({}),
5060 }],
5061 ®istry,
5062 None,
5063 );
5064 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
5065 assert!(first_tool_result_is_masked(&masked));
5066 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
5067 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
5068 }
5069
5070 struct DelegatingFilterCap {
5073 id: &'static str,
5074 inner: std::sync::Arc<InnerFilterCap>,
5075 }
5076 struct InnerFilterCap;
5077
5078 impl Capability for InnerFilterCap {
5079 fn id(&self) -> &str {
5080 "inner_filter"
5081 }
5082 fn name(&self) -> &str {
5083 "Inner Filter"
5084 }
5085 fn description(&self) -> &str {
5086 "inner"
5087 }
5088 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5089 Some(std::sync::Arc::new(SentinelFilter))
5090 }
5091 }
5092 struct SentinelFilter;
5093 impl MessageFilterProvider for SentinelFilter {
5094 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
5095 }
5096 impl Capability for DelegatingFilterCap {
5097 fn id(&self) -> &str {
5098 self.id
5099 }
5100 fn name(&self) -> &str {
5101 "Delegating Filter"
5102 }
5103 fn description(&self) -> &str {
5104 "delegating"
5105 }
5106 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5107 None }
5109 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5110 Some(&*self.inner)
5111 }
5112 }
5113
5114 #[test]
5115 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
5116 let inner = std::sync::Arc::new(InnerFilterCap);
5117 let outer = DelegatingFilterCap {
5118 id: "delegating_filter",
5119 inner: inner.clone(),
5120 };
5121
5122 let mut registry = CapabilityRegistry::new();
5123 registry.register(outer);
5124
5125 let configs = vec![AgentCapabilityConfig {
5126 capability_ref: CapabilityId::new("delegating_filter"),
5127 config: serde_json::json!({}),
5128 }];
5129
5130 let collected = collect_message_filters_only(&configs, ®istry);
5133 assert_eq!(
5134 collected.message_filter_providers.len(),
5135 1,
5136 "provider from resolved inner capability must be collected"
5137 );
5138 }
5139
5140 struct DelegatingMvpCap {
5141 id: &'static str,
5142 inner: std::sync::Arc<InnerMvpCap>,
5143 }
5144 struct InnerMvpCap;
5145
5146 impl Capability for InnerMvpCap {
5147 fn id(&self) -> &str {
5148 "inner_mvp"
5149 }
5150 fn name(&self) -> &str {
5151 "Inner MVP"
5152 }
5153 fn description(&self) -> &str {
5154 "inner"
5155 }
5156 fn model_view_provider(
5157 &self,
5158 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5159 struct NoopMvp;
5161 impl crate::capabilities::ModelViewProvider for NoopMvp {
5162 fn apply_model_view(
5163 &self,
5164 messages: Vec<Message>,
5165 _config: &serde_json::Value,
5166 _context: &ModelViewContext<'_>,
5167 ) -> Vec<Message> {
5168 messages
5169 }
5170 }
5171 Some(std::sync::Arc::new(NoopMvp))
5172 }
5173 }
5174 impl Capability for DelegatingMvpCap {
5175 fn id(&self) -> &str {
5176 self.id
5177 }
5178 fn name(&self) -> &str {
5179 "Delegating MVP"
5180 }
5181 fn description(&self) -> &str {
5182 "delegating"
5183 }
5184 fn model_view_provider(
5185 &self,
5186 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5187 None }
5189 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5190 Some(&*self.inner)
5191 }
5192 }
5193
5194 #[test]
5195 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
5196 let inner = std::sync::Arc::new(InnerMvpCap);
5197 let outer = DelegatingMvpCap {
5198 id: "delegating_mvp",
5199 inner: inner.clone(),
5200 };
5201
5202 let mut registry = CapabilityRegistry::new();
5203 registry.register(outer);
5204
5205 let configs = vec![AgentCapabilityConfig {
5206 capability_ref: CapabilityId::new("delegating_mvp"),
5207 config: serde_json::json!({}),
5208 }];
5209
5210 let collected = collect_model_view_providers(&configs, ®istry, None);
5213 assert_eq!(
5214 collected.model_view_providers.len(),
5215 1,
5216 "provider from resolved inner capability must be collected"
5217 );
5218 }
5219
5220 #[tokio::test]
5230 async fn test_bashkit_shell_capability_produces_bash_tool() {
5231 let registry = CapabilityRegistry::with_builtins();
5232 let collected =
5233 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5234
5235 let tool_names: Vec<&str> = collected
5236 .tool_definitions
5237 .iter()
5238 .map(|t| t.name())
5239 .collect();
5240 assert!(
5241 tool_names.contains(&"bash"),
5242 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5243 tool_names
5244 );
5245 assert!(
5246 !collected.tools.is_empty(),
5247 "bashkit_shell must provide tool implementations"
5248 );
5249 }
5250
5251 #[tokio::test]
5252 async fn test_generic_harness_capability_set_produces_bash_tool() {
5253 let generic_harness_caps = vec![
5256 "session_file_system".to_string(),
5257 "bashkit_shell".to_string(),
5258 "web_fetch".to_string(),
5259 "session_storage".to_string(),
5260 "session".to_string(),
5261 "agent_instructions".to_string(),
5262 "skills".to_string(),
5263 "infinity_context".to_string(),
5264 "auto_tool_search".to_string(),
5265 ];
5266
5267 let registry = CapabilityRegistry::with_builtins();
5268 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5269
5270 let tool_names: Vec<&str> = collected
5271 .tool_definitions
5272 .iter()
5273 .map(|t| t.name())
5274 .collect();
5275 assert!(
5276 tool_names.contains(&"bash"),
5277 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5278 tool_names
5279 );
5280 }
5281
5282 #[tokio::test]
5283 async fn test_collect_capabilities_tool_count_matches_definitions() {
5284 let registry = CapabilityRegistry::with_builtins();
5287 let collected =
5288 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5289
5290 assert_eq!(
5291 collected.tools.len(),
5292 collected.tool_definitions.len(),
5293 "tool implementations ({}) must match tool definitions ({})",
5294 collected.tools.len(),
5295 collected.tool_definitions.len(),
5296 );
5297 }
5298
5299 #[tokio::test]
5303 async fn test_collect_capabilities_resolves_dependencies() {
5304 let registry = CapabilityRegistry::with_builtins();
5307 let collected =
5308 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5309
5310 assert!(
5312 collected
5313 .applied_ids
5314 .iter()
5315 .any(|id| id == "session_file_system"),
5316 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5317 collected.applied_ids
5318 );
5319
5320 let tool_names: Vec<&str> = collected
5321 .tool_definitions
5322 .iter()
5323 .map(|t| t.name())
5324 .collect();
5325
5326 assert!(
5328 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5329 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5330 tool_names
5331 );
5332
5333 assert_eq!(
5335 collected.tools.len(),
5336 collected.tool_definitions.len(),
5337 "dependency-added tools must have implementations, not just definitions"
5338 );
5339 }
5340
5341 #[test]
5342 fn test_defaults_do_not_include_bash() {
5343 let registry = crate::ToolRegistry::with_defaults();
5346 assert!(
5347 !registry.has("bash"),
5348 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5349 );
5350 }
5351
5352 #[tokio::test]
5359 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5360 let registry = CapabilityRegistry::with_builtins();
5361 let collected =
5362 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5363
5364 let tool_names: Vec<&str> = collected
5365 .tool_definitions
5366 .iter()
5367 .map(|t| t.name())
5368 .collect();
5369 assert!(
5370 tool_names.contains(&"spawn_background"),
5371 "spawn_background must be auto-activated when bashkit_shell (a \
5372 background-capable tool) is in the agent's capability set; got: {:?}",
5373 tool_names
5374 );
5375 assert!(
5376 collected
5377 .applied_ids
5378 .iter()
5379 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5380 "background_execution must be in applied_ids when auto-activated; \
5381 got: {:?}",
5382 collected.applied_ids
5383 );
5384
5385 assert!(
5387 collected
5388 .tools
5389 .iter()
5390 .any(|t| t.name() == "spawn_background"),
5391 "spawn_background tool implementation must be present alongside the \
5392 definition (lockstep contract)"
5393 );
5394 }
5395
5396 #[tokio::test]
5399 async fn test_background_execution_does_not_auto_activate_without_hint() {
5400 let registry = CapabilityRegistry::with_builtins();
5401 let collected =
5403 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5404
5405 let tool_names: Vec<&str> = collected
5406 .tool_definitions
5407 .iter()
5408 .map(|t| t.name())
5409 .collect();
5410 assert!(
5411 !tool_names.contains(&"spawn_background"),
5412 "spawn_background must NOT be activated without a background-capable \
5413 tool; got: {:?}",
5414 tool_names
5415 );
5416 assert!(
5417 !collected
5418 .applied_ids
5419 .iter()
5420 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5421 "background_execution must not appear in applied_ids when no \
5422 background-capable tool is present; got: {:?}",
5423 collected.applied_ids
5424 );
5425 }
5426
5427 #[tokio::test]
5428 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5429 let registry = CapabilityRegistry::with_builtins();
5430 let collected = collect_capabilities(
5431 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5432 ®istry,
5433 &test_ctx(),
5434 )
5435 .await;
5436
5437 assert!(
5438 collected
5439 .tools
5440 .iter()
5441 .any(|tool| tool.name() == "spawn_agent"),
5442 "subagent-only sessions should get the unified spawn_agent adapter"
5443 );
5444 let spawn_agent = collected
5445 .tool_definitions
5446 .iter()
5447 .find(|tool| tool.name() == "spawn_agent")
5448 .expect("spawn_agent definition");
5449 assert_eq!(
5450 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5451 serde_json::json!(["subagent"])
5452 );
5453 assert_eq!(
5454 spawn_agent.concurrency_class(),
5455 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5456 "unified spawn_agent must serialize same-batch spawns before cap checks"
5457 );
5458 }
5459
5460 #[tokio::test]
5461 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5462 let mut registry = CapabilityRegistry::new();
5463 registry.register(AgentHandoffCapability);
5464 let agent_id = crate::typed_id::AgentId::new();
5465 let harness_id = crate::typed_id::HarnessId::new();
5466 let configs = vec![AgentCapabilityConfig {
5467 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5468 config: serde_json::json!({
5469 "targets": [{
5470 "id": "aws_operator",
5471 "name": "AWS Operator",
5472 "agent_id": agent_id,
5473 "harness_id": harness_id
5474 }]
5475 }),
5476 }];
5477 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5478
5479 assert!(
5480 collected
5481 .tools
5482 .iter()
5483 .any(|tool| tool.name() == "spawn_agent"),
5484 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5485 );
5486 let spawn_agent = collected
5487 .tool_definitions
5488 .iter()
5489 .find(|tool| tool.name() == "spawn_agent")
5490 .expect("spawn_agent definition");
5491 assert_eq!(
5492 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5493 serde_json::json!(["agent"])
5494 );
5495 }
5496
5497 #[tokio::test]
5498 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5499 let mut registry = CapabilityRegistry::new();
5500 registry.register(SubagentCapability);
5501 registry.register(AgentHandoffCapability);
5502
5503 let agent_id = crate::typed_id::AgentId::new();
5504 let harness_id = crate::typed_id::HarnessId::new();
5505 let configs = vec![
5506 AgentCapabilityConfig {
5507 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5508 config: serde_json::json!({}),
5509 },
5510 AgentCapabilityConfig {
5511 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5512 config: serde_json::json!({
5513 "targets": [{
5514 "id": "aws_operator",
5515 "name": "AWS Operator",
5516 "agent_id": agent_id,
5517 "harness_id": harness_id
5518 }]
5519 }),
5520 },
5521 ];
5522
5523 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5524 let spawn_agent_defs: Vec<_> = collected
5525 .tool_definitions
5526 .iter()
5527 .filter(|tool| tool.name() == "spawn_agent")
5528 .collect();
5529
5530 assert_eq!(spawn_agent_defs.len(), 1);
5531 let schema = spawn_agent_defs[0].parameters();
5532 assert_eq!(
5533 schema["properties"]["target"]["properties"]["type"]["enum"],
5534 serde_json::json!(["subagent", "agent"])
5535 );
5536 assert!(schema.get("oneOf").is_none());
5539 assert!(schema.get("anyOf").is_none());
5540 assert!(schema.get("allOf").is_none());
5541 assert_eq!(
5542 schema["required"],
5543 serde_json::json!(["name", "instructions", "target"])
5544 );
5545 assert_eq!(
5546 schema["properties"]["target"]["oneOf"],
5547 serde_json::json!([
5548 {
5549 "properties": {"type": {"const": "subagent"}}
5550 },
5551 {
5552 "properties": {"type": {"const": "agent"}},
5553 "required": ["type", "id"]
5554 }
5555 ])
5556 );
5557 }
5558
5559 #[cfg(feature = "a2a")]
5560 #[tokio::test]
5561 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5562 let mut registry = CapabilityRegistry::new();
5563 registry.register(SubagentCapability);
5564 registry.register(A2aAgentDelegationCapability);
5565
5566 let configs = vec![
5567 AgentCapabilityConfig {
5568 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5569 config: serde_json::json!({}),
5570 },
5571 AgentCapabilityConfig {
5572 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5573 config: serde_json::json!({
5574 "agents": [{
5575 "id": "local_app",
5576 "name": "Local App",
5577 "base_url": "https://example.com"
5578 }]
5579 }),
5580 },
5581 ];
5582
5583 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5584 let spawn_agent_defs: Vec<_> = collected
5585 .tool_definitions
5586 .iter()
5587 .filter(|tool| tool.name() == "spawn_agent")
5588 .collect();
5589
5590 assert_eq!(spawn_agent_defs.len(), 1);
5591 assert_eq!(
5592 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5593 serde_json::json!(["subagent", "external_a2a"])
5594 );
5595 assert_eq!(
5596 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5597 serde_json::json!(["background", "foreground"])
5598 );
5599 assert!(
5600 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5601 .as_str()
5602 .expect("mode description")
5603 .contains("wait")
5604 );
5605 let schema = spawn_agent_defs[0].parameters();
5606 assert!(schema.get("oneOf").is_none());
5607 assert_eq!(
5611 schema["required"],
5612 serde_json::json!(["name", "instructions", "target"])
5613 );
5614 assert_eq!(
5615 schema["properties"]["target"]["oneOf"],
5616 serde_json::json!([
5617 {
5618 "properties": {"type": {"const": "subagent"}}
5619 },
5620 {
5621 "properties": {"type": {"const": "external_a2a"}},
5622 "anyOf": [
5623 {"required": ["id"]},
5624 {"required": ["external_agent_id"]}
5625 ]
5626 }
5627 ])
5628 );
5629 }
5630
5631 struct ExistingSpawnAgentCapability;
5632
5633 impl Capability for ExistingSpawnAgentCapability {
5634 fn id(&self) -> &str {
5635 "existing_spawn_agent"
5636 }
5637
5638 fn name(&self) -> &str {
5639 "Existing Spawn Agent"
5640 }
5641
5642 fn description(&self) -> &str {
5643 "Test capability that already owns spawn_agent"
5644 }
5645
5646 fn tools(&self) -> Vec<Box<dyn Tool>> {
5647 vec![Box::new(ExistingSpawnAgentTool)]
5648 }
5649 }
5650
5651 struct ExistingSpawnAgentTool;
5652
5653 #[async_trait]
5654 impl Tool for ExistingSpawnAgentTool {
5655 fn name(&self) -> &str {
5656 "spawn_agent"
5657 }
5658
5659 fn description(&self) -> &str {
5660 "Existing spawn_agent test tool"
5661 }
5662
5663 fn parameters_schema(&self) -> serde_json::Value {
5664 serde_json::json!({
5665 "type": "object",
5666 "properties": {
5667 "target": {
5668 "type": "object",
5669 "properties": {
5670 "type": {"type": "string", "enum": ["external_a2a"]}
5671 },
5672 "required": ["type"]
5673 }
5674 },
5675 "required": ["target"]
5676 })
5677 }
5678
5679 async fn execute(
5680 &self,
5681 _arguments: serde_json::Value,
5682 ) -> crate::tools::ToolExecutionResult {
5683 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5684 }
5685 }
5686
5687 #[tokio::test]
5688 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5689 let mut registry = CapabilityRegistry::new();
5690 registry.register(SubagentCapability);
5691 registry.register(ExistingSpawnAgentCapability);
5692
5693 let collected = collect_capabilities(
5694 &[
5695 SUBAGENTS_CAPABILITY_ID.to_string(),
5696 "existing_spawn_agent".to_string(),
5697 ],
5698 ®istry,
5699 &test_ctx(),
5700 )
5701 .await;
5702
5703 let spawn_agent_defs: Vec<_> = collected
5704 .tool_definitions
5705 .iter()
5706 .filter(|tool| tool.name() == "spawn_agent")
5707 .collect();
5708 assert_eq!(spawn_agent_defs.len(), 1);
5709 assert_eq!(
5710 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5711 serde_json::json!(["external_a2a"])
5712 );
5713 }
5714
5715 #[tokio::test]
5716 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5717 let mut registry = CapabilityRegistry::new();
5718 registry.register(AgentHandoffCapability);
5719 registry.register(ExistingSpawnAgentCapability);
5720
5721 let agent_id = crate::typed_id::AgentId::new();
5722 let harness_id = crate::typed_id::HarnessId::new();
5723 let configs = vec![
5724 AgentCapabilityConfig {
5725 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5726 config: serde_json::json!({
5727 "targets": [{
5728 "id": "aws_operator",
5729 "name": "AWS Operator",
5730 "agent_id": agent_id,
5731 "harness_id": harness_id
5732 }]
5733 }),
5734 },
5735 AgentCapabilityConfig {
5736 capability_ref: CapabilityId::new("existing_spawn_agent"),
5737 config: serde_json::json!({}),
5738 },
5739 ];
5740
5741 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5742
5743 let spawn_agent_defs: Vec<_> = collected
5744 .tool_definitions
5745 .iter()
5746 .filter(|tool| tool.name() == "spawn_agent")
5747 .collect();
5748 assert_eq!(spawn_agent_defs.len(), 1);
5749 assert_eq!(
5750 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5751 serde_json::json!(["external_a2a"])
5752 );
5753 }
5754
5755 #[tokio::test]
5759 async fn test_background_execution_explicit_selection_is_idempotent() {
5760 let registry = CapabilityRegistry::with_builtins();
5761 let collected = collect_capabilities(
5762 &[
5763 "bashkit_shell".to_string(),
5764 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5765 ],
5766 ®istry,
5767 &test_ctx(),
5768 )
5769 .await;
5770
5771 let spawn_background_count = collected
5772 .tool_definitions
5773 .iter()
5774 .filter(|t| t.name() == "spawn_background")
5775 .count();
5776 assert_eq!(
5777 spawn_background_count, 1,
5778 "spawn_background must appear exactly once even when \
5779 background_execution is selected explicitly alongside a \
5780 background-capable tool"
5781 );
5782 let applied_count = collected
5783 .applied_ids
5784 .iter()
5785 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5786 .count();
5787 assert_eq!(
5788 applied_count, 1,
5789 "background_execution must appear exactly once in applied_ids"
5790 );
5791 }
5792
5793 #[test]
5798 fn test_defaults_do_not_include_spawn_background() {
5799 let registry = crate::ToolRegistry::with_defaults();
5800 assert!(
5801 !registry.has("spawn_background"),
5802 "with_defaults() must not include 'spawn_background' — it comes \
5803 from the background_execution capability (EVE-501)"
5804 );
5805 }
5806
5807 #[test]
5812 fn test_capability_features_default_empty() {
5813 let registry = CapabilityRegistry::with_builtins();
5814
5815 let noop = registry.get("noop").unwrap();
5817 assert!(noop.features().is_empty());
5818
5819 let current_time = registry.get("current_time").unwrap();
5820 assert!(current_time.features().is_empty());
5821 }
5822
5823 #[test]
5824 fn test_file_system_capability_features() {
5825 let registry = CapabilityRegistry::with_builtins();
5826
5827 let fs = registry.get("session_file_system").unwrap();
5828 assert_eq!(fs.features(), vec!["file_system"]);
5829 }
5830
5831 #[test]
5832 fn test_bashkit_shell_capability_features() {
5833 let registry = CapabilityRegistry::with_builtins();
5834
5835 let bash = registry.get("bashkit_shell").unwrap();
5836 assert_eq!(bash.features(), vec!["file_system"]);
5837 }
5838
5839 #[test]
5840 fn test_alias_resolves_to_canonical_capability() {
5841 let registry = CapabilityRegistry::with_builtins();
5842
5843 let via_alias = registry.get("virtual_bash").unwrap();
5845 assert_eq!(via_alias.id(), "bashkit_shell");
5846 assert!(registry.has("virtual_bash"));
5847 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5848 assert_eq!(
5849 registry.canonical_id("bashkit_shell"),
5850 Some("bashkit_shell")
5851 );
5852 assert_eq!(registry.canonical_id("nonexistent"), None);
5853 }
5854
5855 #[test]
5856 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5857 let registry = CapabilityRegistry::with_builtins();
5858
5859 let resolved = resolve_dependencies(
5862 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5863 ®istry,
5864 )
5865 .unwrap();
5866 let bash_ids: Vec<_> = resolved
5867 .resolved_ids
5868 .iter()
5869 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5870 .collect();
5871 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5872 assert!(
5874 !resolved
5875 .added_as_dependencies
5876 .contains(&"bashkit_shell".to_string())
5877 );
5878 }
5879
5880 #[test]
5881 fn test_alias_preserves_explicit_config_in_resolution() {
5882 let registry = CapabilityRegistry::with_builtins();
5883
5884 let configs = vec![AgentCapabilityConfig::with_config(
5885 "virtual_bash".to_string(),
5886 serde_json::json!({"key": "value"}),
5887 )];
5888 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5889 let bash = resolved
5890 .iter()
5891 .find(|c| c.capability_id() == "bashkit_shell")
5892 .expect("alias must resolve to canonical bashkit_shell config");
5893 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5894 }
5895
5896 #[test]
5897 fn test_unregister_by_alias_removes_capability_and_aliases() {
5898 let mut registry = CapabilityRegistry::with_builtins();
5899
5900 assert!(registry.unregister("virtual_bash").is_some());
5901 assert!(!registry.has("bashkit_shell"));
5902 assert!(!registry.has("virtual_bash"));
5903 }
5904
5905 #[test]
5906 fn test_session_storage_capability_features() {
5907 let registry = CapabilityRegistry::with_builtins();
5908
5909 let storage = registry.get("session_storage").unwrap();
5910 let features = storage.features();
5911 assert!(features.contains(&"secrets"));
5912 assert!(features.contains(&"key_value"));
5913 }
5914
5915 #[test]
5916 fn test_session_schedule_capability_features() {
5917 let registry = CapabilityRegistry::with_builtins();
5918
5919 let schedule = registry.get("session_schedule").unwrap();
5920 assert_eq!(schedule.features(), vec!["schedules"]);
5921 }
5922
5923 #[test]
5924 fn test_session_sql_database_capability_features() {
5925 let registry = CapabilityRegistry::with_builtins();
5926
5927 let sql = registry.get("session_sql_database").unwrap();
5928 assert_eq!(sql.features(), vec!["sql_database"]);
5929 }
5930
5931 #[test]
5932 fn test_sample_data_capability_features() {
5933 let registry = CapabilityRegistry::with_builtins();
5934
5935 let sample = registry.get("sample_data").unwrap();
5936 assert_eq!(sample.features(), vec!["file_system"]);
5937 }
5938
5939 #[test]
5940 fn test_compute_features_empty() {
5941 let registry = CapabilityRegistry::with_builtins();
5942
5943 let features = compute_features(&[], ®istry);
5944 assert!(features.is_empty());
5945 }
5946
5947 #[test]
5948 fn test_compute_features_single_capability() {
5949 let registry = CapabilityRegistry::with_builtins();
5950
5951 let features = compute_features(&["session_schedule".to_string()], ®istry);
5952 assert_eq!(features, vec!["schedules"]);
5953 }
5954
5955 #[test]
5956 fn test_compute_features_multiple_capabilities() {
5957 let registry = CapabilityRegistry::with_builtins();
5958
5959 let features = compute_features(
5960 &[
5961 "session_file_system".to_string(),
5962 "session_storage".to_string(),
5963 "session_schedule".to_string(),
5964 ],
5965 ®istry,
5966 );
5967 assert!(features.contains(&"file_system".to_string()));
5968 assert!(features.contains(&"secrets".to_string()));
5969 assert!(features.contains(&"key_value".to_string()));
5970 assert!(features.contains(&"schedules".to_string()));
5971 }
5972
5973 #[test]
5974 fn test_compute_features_deduplicates() {
5975 let registry = CapabilityRegistry::with_builtins();
5976
5977 let features = compute_features(
5979 &[
5980 "session_file_system".to_string(),
5981 "bashkit_shell".to_string(),
5982 ],
5983 ®istry,
5984 );
5985 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5986 assert_eq!(file_system_count, 1, "file_system should appear only once");
5987 }
5988
5989 #[test]
5990 fn test_compute_features_includes_dependency_features() {
5991 let registry = CapabilityRegistry::with_builtins();
5992
5993 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5995 assert!(features.contains(&"file_system".to_string()));
5996 }
5997
5998 #[test]
5999 fn test_compute_features_generic_harness_set() {
6000 let registry = CapabilityRegistry::with_builtins();
6001
6002 let features = compute_features(
6004 &[
6005 "session_file_system".to_string(),
6006 "bashkit_shell".to_string(),
6007 "session_storage".to_string(),
6008 "session".to_string(),
6009 "session_schedule".to_string(),
6010 ],
6011 ®istry,
6012 );
6013 assert!(features.contains(&"file_system".to_string()));
6014 assert!(features.contains(&"secrets".to_string()));
6015 assert!(features.contains(&"key_value".to_string()));
6016 assert!(features.contains(&"schedules".to_string()));
6017 }
6018
6019 #[test]
6020 fn test_compute_features_unknown_capability_ignored() {
6021 let registry = CapabilityRegistry::with_builtins();
6022
6023 let features = compute_features(
6024 &["unknown_cap".to_string(), "session_schedule".to_string()],
6025 ®istry,
6026 );
6027 assert_eq!(features, vec!["schedules"]);
6028 }
6029
6030 #[test]
6031 fn test_risk_level_ordering() {
6032 assert!(RiskLevel::Low < RiskLevel::Medium);
6033 assert!(RiskLevel::Medium < RiskLevel::High);
6034 }
6035
6036 #[test]
6037 fn test_risk_level_serde_roundtrip() {
6038 let high = RiskLevel::High;
6039 let json = serde_json::to_string(&high).unwrap();
6040 assert_eq!(json, "\"high\"");
6041 let back: RiskLevel = serde_json::from_str(&json).unwrap();
6042 assert_eq!(back, RiskLevel::High);
6043 }
6044
6045 #[test]
6046 fn test_capability_risk_levels() {
6047 let registry = CapabilityRegistry::with_builtins();
6048
6049 let bash = registry.get("bashkit_shell").unwrap();
6051 assert_eq!(bash.risk_level(), RiskLevel::High);
6052
6053 let fetch = registry.get("web_fetch").unwrap();
6055 assert_eq!(fetch.risk_level(), RiskLevel::High);
6056
6057 let noop = registry.get("noop").unwrap();
6059 assert_eq!(noop.risk_level(), RiskLevel::Low);
6060 }
6061
6062 #[tokio::test]
6067 async fn test_apply_capabilities_openai_tool_search() {
6068 let registry = CapabilityRegistry::with_builtins();
6069 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6070
6071 let applied = apply_capabilities(
6072 base_runtime_agent.clone(),
6073 &["openai_tool_search".to_string()],
6074 ®istry,
6075 &test_ctx(),
6076 )
6077 .await;
6078
6079 assert_eq!(
6081 applied.runtime_agent.system_prompt,
6082 base_runtime_agent.system_prompt
6083 );
6084 assert!(applied.tool_registry.is_empty());
6085 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
6086
6087 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6089 assert!(ts.enabled);
6090 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6091 }
6092
6093 #[tokio::test]
6094 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
6095 let registry = CapabilityRegistry::with_builtins();
6096 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6097
6098 let applied = apply_capabilities(
6099 base_runtime_agent,
6100 &[
6101 "current_time".to_string(),
6102 "openai_tool_search".to_string(),
6103 "test_math".to_string(),
6104 ],
6105 ®istry,
6106 &test_ctx(),
6107 )
6108 .await;
6109
6110 assert!(applied.tool_registry.has("get_current_time"));
6112 assert!(applied.tool_registry.has("add"));
6113 assert!(applied.tool_registry.has("subtract"));
6114 assert!(applied.tool_registry.has("multiply"));
6115 assert!(applied.tool_registry.has("divide"));
6116
6117 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6119 assert!(ts.enabled);
6120 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6121 }
6122
6123 #[tokio::test]
6124 async fn test_collect_capabilities_tool_search_custom_threshold() {
6125 let registry = CapabilityRegistry::with_builtins();
6126
6127 let configs = vec![AgentCapabilityConfig {
6128 capability_ref: CapabilityId::new("openai_tool_search"),
6129 config: serde_json::json!({"threshold": 5}),
6130 }];
6131
6132 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6133
6134 let ts = collected.tool_search.as_ref().unwrap();
6135 assert!(ts.enabled);
6136 assert_eq!(ts.threshold, 5);
6137 }
6138
6139 #[tokio::test]
6140 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
6141 let registry = CapabilityRegistry::with_builtins();
6142
6143 let configs = vec![
6144 AgentCapabilityConfig {
6145 capability_ref: CapabilityId::new("auto_tool_search"),
6146 config: serde_json::json!({"threshold": 2}),
6147 },
6148 AgentCapabilityConfig {
6149 capability_ref: CapabilityId::new("test_math"),
6150 config: serde_json::json!({}),
6151 },
6152 ];
6153
6154 let ctx = test_ctx().with_model("claude-3-5-haiku");
6158 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6159
6160 assert!(
6161 collected.tool_search.is_none(),
6162 "auto_tool_search must not set a hosted config on a non-native model"
6163 );
6164 assert!(
6165 collected
6166 .tools
6167 .iter()
6168 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6169 "auto_tool_search must contribute the client-side tool_search tool"
6170 );
6171 assert!(
6172 !collected.tool_definition_hooks.is_empty(),
6173 "auto_tool_search must contribute a client-side deferral hook"
6174 );
6175
6176 let mut transformed = collected.tool_definitions.clone();
6177 for hook in &collected.tool_definition_hooks {
6178 transformed = hook.transform(transformed);
6179 }
6180 let add_tool = transformed
6181 .iter()
6182 .find(|tool| tool.name() == "add")
6183 .expect("test_math contributes add");
6184 assert!(
6185 add_tool.parameters().get("properties").is_none(),
6186 "generic auto_tool_search must honor the configured threshold"
6187 );
6188 }
6189
6190 #[tokio::test]
6191 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
6192 let registry = CapabilityRegistry::with_builtins();
6193
6194 let configs = vec![AgentCapabilityConfig {
6195 capability_ref: CapabilityId::new("auto_tool_search"),
6196 config: serde_json::json!({"threshold": 7}),
6197 }];
6198
6199 let ctx = test_ctx().with_model("gpt-5.4");
6202 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6203
6204 let ts = collected
6205 .tool_search
6206 .as_ref()
6207 .expect("auto_tool_search must set a hosted config on a native model");
6208 assert!(ts.enabled);
6209 assert_eq!(ts.threshold, 7);
6210 assert!(
6211 !collected
6212 .tools
6213 .iter()
6214 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6215 "hosted mechanism must not contribute the client-side tool_search tool"
6216 );
6217 assert!(
6218 collected.tool_definition_hooks.is_empty(),
6219 "hosted mechanism must not contribute a client-side deferral hook"
6220 );
6221 }
6222
6223 #[tokio::test]
6224 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6225 let registry = CapabilityRegistry::with_builtins();
6226
6227 let configs = vec![AgentCapabilityConfig {
6228 capability_ref: CapabilityId::new("auto_tool_search"),
6229 config: serde_json::json!({"threshold": 9}),
6230 }];
6231
6232 let ctx = test_ctx().with_model("claude-opus-4-8");
6235 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6236
6237 let ts = collected
6238 .tool_search
6239 .as_ref()
6240 .expect("auto_tool_search must set a hosted config on a native Claude model");
6241 assert!(ts.enabled);
6242 assert_eq!(ts.threshold, 9);
6243 assert!(
6244 !collected
6245 .tools
6246 .iter()
6247 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6248 "hosted mechanism must not contribute the client-side tool_search tool"
6249 );
6250 assert!(
6251 collected.tool_definition_hooks.is_empty(),
6252 "hosted mechanism must not contribute a client-side deferral hook"
6253 );
6254 }
6255
6256 #[tokio::test]
6257 async fn test_collect_capabilities_no_tool_search_without_capability() {
6258 let registry = CapabilityRegistry::with_builtins();
6259
6260 let configs = vec![AgentCapabilityConfig {
6261 capability_ref: CapabilityId::new("current_time"),
6262 config: serde_json::json!({}),
6263 }];
6264
6265 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6266
6267 assert!(collected.tool_search.is_none());
6268 }
6269
6270 #[tokio::test]
6271 async fn test_collect_capabilities_tool_search_category_propagation() {
6272 let registry = CapabilityRegistry::with_builtins();
6273
6274 let configs = vec![
6276 AgentCapabilityConfig {
6277 capability_ref: CapabilityId::new("test_math"),
6278 config: serde_json::json!({}),
6279 },
6280 AgentCapabilityConfig {
6281 capability_ref: CapabilityId::new("openai_tool_search"),
6282 config: serde_json::json!({}),
6283 },
6284 ];
6285
6286 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6287
6288 assert!(collected.tool_search.is_some());
6290
6291 for tool_def in &collected.tool_definitions {
6293 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6295 assert!(
6296 tool_def.category().is_some(),
6297 "Tool {} should have a category from its capability",
6298 tool_def.name()
6299 );
6300 }
6301 }
6302 }
6303
6304 #[tokio::test]
6305 async fn test_apply_capabilities_prompt_caching() {
6306 let registry = CapabilityRegistry::with_builtins();
6307 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6308
6309 let applied = apply_capabilities(
6310 base_runtime_agent.clone(),
6311 &["prompt_caching".to_string()],
6312 ®istry,
6313 &test_ctx(),
6314 )
6315 .await;
6316
6317 assert_eq!(
6318 applied.runtime_agent.system_prompt,
6319 base_runtime_agent.system_prompt
6320 );
6321 assert!(applied.tool_registry.is_empty());
6322 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6323
6324 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6325 assert!(prompt_cache.enabled);
6326 assert_eq!(
6327 prompt_cache.strategy,
6328 crate::driver_registry::PromptCacheStrategy::Auto
6329 );
6330 assert!(prompt_cache.gemini_cached_content.is_none());
6331 }
6332
6333 #[tokio::test]
6334 async fn test_apply_capabilities_openrouter_server_tools() {
6335 let registry = CapabilityRegistry::with_builtins();
6336 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6337
6338 let configs = vec![AgentCapabilityConfig {
6339 capability_ref: CapabilityId::new("openrouter_server_tools"),
6340 config: serde_json::json!({
6341 "tools": ["web_search", "datetime"],
6342 "web_search_max_results": 4,
6343 }),
6344 }];
6345
6346 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6347 let routing = collected
6348 .openrouter_routing
6349 .as_ref()
6350 .expect("server tools produce routing config");
6351 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6352 assert_eq!(
6353 kinds,
6354 vec![
6355 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6356 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6357 ]
6358 );
6359
6360 let applied = apply_capabilities(
6363 base_runtime_agent,
6364 &["openrouter_server_tools".to_string()],
6365 ®istry,
6366 &test_ctx(),
6367 )
6368 .await;
6369 assert!(applied.tool_registry.is_empty());
6370 assert!(applied.runtime_agent.openrouter_routing.is_none());
6371 }
6372
6373 #[tokio::test]
6374 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6375 let registry = CapabilityRegistry::with_builtins();
6376
6377 let configs = vec![AgentCapabilityConfig {
6378 capability_ref: CapabilityId::new("prompt_caching"),
6379 config: serde_json::json!({"strategy": "auto"}),
6380 }];
6381
6382 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6383
6384 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6385 assert!(prompt_cache.enabled);
6386 assert_eq!(
6387 prompt_cache.strategy,
6388 crate::driver_registry::PromptCacheStrategy::Auto
6389 );
6390 assert!(prompt_cache.gemini_cached_content.is_none());
6391 }
6392
6393 #[tokio::test]
6394 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6395 let registry = CapabilityRegistry::with_builtins();
6396
6397 let configs = vec![AgentCapabilityConfig {
6398 capability_ref: CapabilityId::new("prompt_caching"),
6399 config: serde_json::json!({
6400 "strategy": "auto",
6401 "gemini_cached_content": "cachedContents/demo-cache"
6402 }),
6403 }];
6404
6405 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6406
6407 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6408 assert_eq!(
6409 prompt_cache.gemini_cached_content.as_deref(),
6410 Some("cachedContents/demo-cache")
6411 );
6412 }
6413
6414 #[tokio::test]
6415 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6416 let registry = CapabilityRegistry::with_builtins();
6417
6418 let collected = collect_capabilities_with_configs(
6420 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6421 ®istry,
6422 &test_ctx(),
6423 )
6424 .await;
6425 assert_eq!(collected.parallel_tool_calls, Some(true));
6426
6427 let collected = collect_capabilities_with_configs(
6429 &[AgentCapabilityConfig {
6430 capability_ref: CapabilityId::new("parallel_tool_calls"),
6431 config: serde_json::json!({"mode": "avoid"}),
6432 }],
6433 ®istry,
6434 &test_ctx(),
6435 )
6436 .await;
6437 assert_eq!(collected.parallel_tool_calls, Some(false));
6438
6439 let collected = collect_capabilities_with_configs(
6441 &[AgentCapabilityConfig {
6442 capability_ref: CapabilityId::new("parallel_tool_calls"),
6443 config: serde_json::json!({"mode": "none"}),
6444 }],
6445 ®istry,
6446 &test_ctx(),
6447 )
6448 .await;
6449 assert_eq!(collected.parallel_tool_calls, None);
6450
6451 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6453 assert_eq!(collected.parallel_tool_calls, None);
6454 }
6455
6456 #[tokio::test]
6457 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6458 let registry = CapabilityRegistry::with_builtins();
6459
6460 let applied = apply_capabilities(
6462 RuntimeAgent::new("p", "gpt-5.2"),
6463 &["parallel_tool_calls".to_string()],
6464 ®istry,
6465 &test_ctx(),
6466 )
6467 .await;
6468 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6469
6470 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6472 base.parallel_tool_calls = Some(false);
6473 let applied = apply_capabilities(
6474 base,
6475 &["parallel_tool_calls".to_string()],
6476 ®istry,
6477 &test_ctx(),
6478 )
6479 .await;
6480 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6481 }
6482
6483 struct SkillContributingCapability;
6488
6489 impl Capability for SkillContributingCapability {
6490 fn id(&self) -> &str {
6491 "contributes_skills"
6492 }
6493 fn name(&self) -> &str {
6494 "Contributes Skills"
6495 }
6496 fn description(&self) -> &str {
6497 "Test capability that contributes skills."
6498 }
6499 fn contribute_skills(&self) -> Vec<SkillContribution> {
6500 vec![
6501 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6502 .with_files(vec![(
6503 "scripts/a.sh".to_string(),
6504 "#!/bin/sh\necho a\n".to_string(),
6505 )]),
6506 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6507 .with_user_invocable(false),
6508 ]
6509 }
6510 }
6511
6512 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6513 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6514 MountSource::InlineFile { content, .. } => content.as_str(),
6515 _ => panic!("Expected InlineFile for SKILL.md"),
6516 }
6517 }
6518
6519 #[tokio::test]
6520 async fn test_contribute_skills_normalized_to_mounts() {
6521 let mut registry = CapabilityRegistry::new();
6522 registry.register(SkillContributingCapability);
6523
6524 let configs = vec![AgentCapabilityConfig {
6525 capability_ref: CapabilityId::new("contributes_skills"),
6526 config: serde_json::json!({}),
6527 }];
6528
6529 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6530
6531 let skill_mounts: Vec<_> = collected
6532 .mounts
6533 .iter()
6534 .filter(|m| m.path.starts_with("/.agents/skills/"))
6535 .collect();
6536 assert_eq!(skill_mounts.len(), 2);
6537
6538 for m in &skill_mounts {
6541 assert!(m.is_readonly());
6542 assert_eq!(m.capability_id, "contributes_skills");
6543 }
6544
6545 let alpha = skill_mounts
6546 .iter()
6547 .find(|m| m.path == "/.agents/skills/alpha-skill")
6548 .expect("alpha-skill mount missing");
6549 match &alpha.source {
6550 MountSource::InlineDirectory { entries } => {
6551 assert!(entries.contains_key("SKILL.md"));
6552 assert!(entries.contains_key("scripts/a.sh"));
6553 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6554 assert_eq!(parsed.name, "alpha-skill");
6555 assert!(parsed.user_invocable);
6556 }
6557 _ => panic!("Expected InlineDirectory"),
6558 }
6559
6560 let beta = skill_mounts
6561 .iter()
6562 .find(|m| m.path == "/.agents/skills/beta-skill")
6563 .expect("beta-skill mount missing");
6564 match &beta.source {
6565 MountSource::InlineDirectory { entries } => {
6566 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6567 assert!(!parsed.user_invocable);
6568 }
6569 _ => panic!("Expected InlineDirectory"),
6570 }
6571 }
6572
6573 #[tokio::test]
6574 async fn test_contribute_skills_default_empty() {
6575 let mut registry = CapabilityRegistry::new();
6578 registry.register(FilterTestCapability { priority: 0 });
6579
6580 let configs = vec![AgentCapabilityConfig {
6581 capability_ref: CapabilityId::new("filter_test"),
6582 config: serde_json::json!({}),
6583 }];
6584
6585 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6586 assert!(
6587 collected
6588 .mounts
6589 .iter()
6590 .all(|m| !m.path.starts_with("/.agents/skills/"))
6591 );
6592 }
6593
6594 struct LocalizedCapability;
6595
6596 impl Capability for LocalizedCapability {
6597 fn id(&self) -> &str {
6598 "localized"
6599 }
6600 fn name(&self) -> &str {
6601 "Localized"
6602 }
6603 fn description(&self) -> &str {
6604 "English description"
6605 }
6606 fn localizations(&self) -> Vec<CapabilityLocalization> {
6607 vec![
6608 CapabilityLocalization {
6609 locale: "en",
6610 name: None,
6611 description: None,
6612 config_description: Some("Controls things."),
6613 config_overlay: None,
6614 },
6615 CapabilityLocalization {
6616 locale: "uk",
6617 name: Some("Локалізована"),
6618 description: Some("Український опис"),
6619 config_description: Some("Керує налаштуваннями."),
6620 config_overlay: None,
6621 },
6622 ]
6623 }
6624 }
6625
6626 #[test]
6627 fn localized_name_falls_back_exact_language_then_base() {
6628 let cap = LocalizedCapability;
6629 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6631 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6632 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6634 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6636 assert_eq!(cap.localized_name(None), "Localized");
6637 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6638 assert_eq!(cap.localized_description(Some("de")), "English description");
6639 }
6640
6641 #[test]
6642 fn describe_schema_resolves_config_description_per_locale() {
6643 let cap = LocalizedCapability;
6644 assert_eq!(
6645 cap.describe_schema(Some("uk-UA")).as_deref(),
6646 Some("Керує налаштуваннями.")
6647 );
6648 assert_eq!(
6650 cap.describe_schema(Some("pl")).as_deref(),
6651 Some("Controls things.")
6652 );
6653 assert_eq!(
6654 cap.describe_schema(None).as_deref(),
6655 Some("Controls things.")
6656 );
6657 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6659 }
6660}