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 platform_management;
133mod progress_guard;
134mod prompt_caching;
135mod prompt_canary_guardrail;
136mod research;
137mod sample_data;
138mod self_budget;
139mod session;
140mod session_sandbox;
141mod session_schedule;
142mod session_sql_database;
143mod session_storage;
144mod session_tasks;
145mod skills;
146mod skills_scoped;
147mod stateless_todo_list;
148mod subagents;
149mod system_commands;
150mod test_math;
151mod test_weather;
152mod tool_approval;
153mod tool_call_repair;
154mod tool_output_distillation;
155mod tool_output_persistence;
156mod tool_search;
157mod usage_limit_auto_continue;
158pub mod user_hooks;
159mod util;
160#[cfg(feature = "web-fetch")]
161mod web_fetch;
162
163pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
168pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
172#[cfg(feature = "a2a")]
173pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
174#[cfg(feature = "ui-capabilities")]
175pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
176pub use agent_handoff::{
177 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
178};
179pub use agent_instructions::{
180 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
181 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
182 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
183};
184pub use attach_skill::{
185 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
186 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
187 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
188};
189pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
190pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
191pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
192pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
193pub use citation_retrieval::{
194 CITATION_RETRIEVAL_CAPABILITY_ID, CitationRetrievalCapability, CitationRetrievalConfig,
195};
196pub use citation_verification::{
197 CITATION_VERIFICATION_CAPABILITY_ID, CitationVerificationCapability,
198 CitationVerificationConfig, VerificationMode,
199};
200pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
201pub use compaction::{
202 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
203 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
204 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
205 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
206 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
207 build_summarization_prompt, build_summary_message, classify_memory_tiers,
208 compose_summary_with_recent, estimate_tokens, estimate_total_tokens,
209 format_messages_for_summarization, should_compact_for_cost, should_compact_proactively,
210 total_tool_result_bytes,
211};
212pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
213pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
214pub use declarative::{
215 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
216 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
217 declarative_capability_info, hydrate_declarative_capability_config,
218 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
219 plugin_capability_info, validate_declarative_capability_definition,
220};
221pub use delegation_result::{
222 ReportResultTool, ReportTaskProgressTool, report_result_tool_for_child_session,
223 report_task_progress_tool_for_child_session,
224};
225pub use error_disclosure::{
226 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
227};
228pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
229pub use fake_aws::{
230 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
231 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
232 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
233 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
234};
235pub use fake_crm::{
236 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
237 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
238 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
239};
240pub use fake_financial::{
241 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
242 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
243 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
244 FinanceListTransactionsTool,
245};
246pub use fake_warehouse::{
247 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
248 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
249 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
250 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
251};
252pub use file_system::{
253 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
254 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
255};
256pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
257pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
258pub use infinity_context::{
259 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, InfinityContextFilterOnlyCapability,
260 QueryHistoryTool,
261};
262pub use knowledge_base::{
263 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
264 validate_knowledge_base_config,
265};
266pub use knowledge_index::{
267 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
268 validate_knowledge_index_config,
269};
270pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
271pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
272pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
273pub use mcp::{
274 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
275 parse_mcp_capability_id,
276};
277pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
278pub use message_metadata::{
279 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
280 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
281};
282pub use model_scout::{
283 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
284 RouterUpdateProposal, compute_score, rank_results,
285};
286pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
287pub use openai_tool_search::{
288 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
289 model_supports_native_tool_search,
290};
291pub use openrouter_server_tools::{
292 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
293};
294pub use openrouter_workspace::{
295 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
296 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
297 detect_policy_drift,
298};
299#[cfg(feature = "ui-capabilities")]
300pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
301pub use parallel_tool_calls::{
302 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
303 parallel_tool_calls_from_config,
304};
305pub use platform_management::{
306 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
307 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
308 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
309};
310pub use progress_guard::{PROGRESS_GUARD_CAPABILITY_ID, ProgressGuardCapability};
311pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
312pub use prompt_canary_guardrail::{
313 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
314 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
315 REASON_CODE_SYSTEM_PROMPT_LEAK,
316};
317pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
318pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
319pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
320pub use session::{
321 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, SessionCapabilityConfig,
322 SessionTitleMutation, WriteSessionTitleTool, session_title_updated_event,
323 update_session_title_with_event,
324};
325pub use session_sandbox::{
326 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
327 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
328};
329pub use session_schedule::{
330 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
331 SessionScheduleCapability,
332};
333pub use session_sql_database::{
334 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
335 SqlSchemaTool,
336};
337pub use session_storage::{
338 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
339 is_internal_session_kv_key,
340};
341pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
342pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
343pub use skills_scoped::{
344 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
345};
346pub use stateless_todo_list::{
347 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
348};
349pub(crate) use subagents::SPAWN_AGENT_CONCURRENCY_CLASS;
350pub use subagents::{SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool, SubagentCapability};
351pub use usage_limit_auto_continue::{
352 AutoContinueConfig, USAGE_LIMIT_AUTO_CONTINUE_CAPABILITY_ID, UsageLimitAutoContinueCapability,
353 resolve_usage_limit_auto_continue,
354};
355pub use bashkit_shell::{
357 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
358};
359pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
360pub use test_math::{
361 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
362};
363pub use test_weather::{
364 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
365};
366pub use tool_approval::{
367 ApprovalDecision, ApprovalMode, TOOL_APPROVAL_CAPABILITY_ID, ToolApprovalCapability,
368 ToolApprover,
369};
370pub use tool_call_repair::{
371 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
372 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
373 salvage_tool_arguments, tool_call_repair_capability,
374};
375pub use tool_output_distillation::{
376 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
377};
378pub use tool_output_persistence::{
379 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
380};
381pub use tool_search::{
382 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
383};
384pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
385#[cfg(feature = "web-fetch")]
386pub use web_fetch::{
387 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
388 derive_bot_auth_public_key,
389};
390
391pub struct SystemPromptContext {
401 pub session_id: SessionId,
403 pub locale: Option<String>,
405 pub file_store: Option<Arc<dyn SessionFileSystem>>,
407 pub model: Option<String>,
413}
414
415impl SystemPromptContext {
416 pub fn without_file_store(session_id: SessionId) -> Self {
418 Self {
419 session_id,
420 locale: None,
421 file_store: None,
422 model: None,
423 }
424 }
425
426 pub fn with_model(mut self, model: impl Into<String>) -> Self {
428 self.model = Some(model.into());
429 self
430 }
431}
432
433#[derive(Debug, Clone)]
485pub struct CapabilityLocalization {
486 pub locale: &'static str,
488 pub name: Option<&'static str>,
490 pub description: Option<&'static str>,
492 pub config_description: Option<&'static str>,
497 pub config_overlay: Option<serde_json::Value>,
503}
504
505impl CapabilityLocalization {
506 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
508 Self {
509 locale,
510 name: Some(name),
511 description: Some(description),
512 config_description: None,
513 config_overlay: None,
514 }
515 }
516}
517
518pub fn resolve_localized_field<T>(
522 localizations: &[CapabilityLocalization],
523 locale: Option<&str>,
524 field: impl Fn(&CapabilityLocalization) -> Option<T>,
525) -> Option<T> {
526 let mut candidates: Vec<String> = Vec::new();
527 if let Some(raw) = locale {
528 let normalized = raw.trim().replace('_', "-").to_lowercase();
529 if !normalized.is_empty() {
530 if let Some((language, _)) = normalized.split_once('-') {
531 let language = language.to_string();
532 candidates.push(normalized);
533 candidates.push(language);
534 } else {
535 candidates.push(normalized);
536 }
537 }
538 }
539 candidates.push("en".to_string());
540
541 for candidate in candidates {
542 let hit = localizations
543 .iter()
544 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
545 .and_then(&field);
546 if hit.is_some() {
547 return hit;
548 }
549 }
550 None
551}
552
553#[async_trait]
554pub trait Capability: Send + Sync {
555 fn id(&self) -> &str;
557
558 fn aliases(&self) -> Vec<&'static str> {
567 vec![]
568 }
569
570 fn name(&self) -> &str;
572
573 fn description(&self) -> &str;
575
576 fn localizations(&self) -> Vec<CapabilityLocalization> {
581 vec![]
582 }
583
584 fn localized_name(&self, locale: Option<&str>) -> String {
587 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
588 .unwrap_or_else(|| self.name())
589 .to_string()
590 }
591
592 fn localized_description(&self, locale: Option<&str>) -> String {
594 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
595 .unwrap_or_else(|| self.description())
596 .to_string()
597 }
598
599 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
603 resolve_localized_field(&self.localizations(), locale, |entry| {
604 entry.config_description
605 })
606 .map(str::to_string)
607 }
608
609 fn status(&self) -> CapabilityStatus {
611 CapabilityStatus::Available
612 }
613
614 fn icon(&self) -> Option<&str> {
616 None
617 }
618
619 fn category(&self) -> Option<&str> {
621 None
622 }
623
624 fn metadata(&self) -> Option<serde_json::Value> {
636 None
637 }
638
639 fn is_guardrail(&self) -> bool {
644 false
645 }
646
647 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
658 None
659 }
660
661 fn system_prompt_addition(&self) -> Option<&str> {
681 None
682 }
683
684 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
696 self.system_prompt_addition().map(|addition| {
697 format!(
698 "<capability id=\"{}\">\n{}\n</capability>",
699 self.id(),
700 addition
701 )
702 })
703 }
704
705 fn system_prompt_preview(&self) -> Option<String> {
711 self.system_prompt_addition().map(|s| s.to_string())
712 }
713
714 fn tools(&self) -> Vec<Box<dyn Tool>> {
716 vec![]
717 }
718
719 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
727 self.tools()
728 }
729
730 async fn system_prompt_contribution_with_config(
737 &self,
738 ctx: &SystemPromptContext,
739 _config: &serde_json::Value,
740 ) -> Option<String> {
741 self.system_prompt_contribution(ctx).await
742 }
743
744 fn tool_definitions(&self) -> Vec<ToolDefinition> {
747 self.tools().iter().map(|t| t.to_definition()).collect()
748 }
749
750 fn mounts(&self) -> Vec<MountPoint> {
758 vec![]
759 }
760
761 fn dependencies(&self) -> Vec<&'static str> {
770 vec![]
771 }
772
773 fn features(&self) -> Vec<&'static str> {
788 vec![]
789 }
790
791 fn config_schema(&self) -> Option<serde_json::Value> {
797 None
798 }
799
800 fn config_ui_schema(&self) -> Option<serde_json::Value> {
805 None
806 }
807
808 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
814 Ok(())
815 }
816
817 fn mcp_servers(&self) -> ScopedMcpServers {
823 ScopedMcpServers::default()
824 }
825
826 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
828 self.mcp_servers()
829 }
830
831 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
844 None
845 }
846
847 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
855 None
856 }
857
858 fn llm_error_hook(&self) -> Option<Arc<dyn crate::llm_error_hook::LlmErrorHook>> {
870 None
871 }
872
873 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
888 vec![]
889 }
890
891 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
902 vec![]
903 }
904
905 fn pre_tool_use_hooks_with_config(
910 &self,
911 _config: &serde_json::Value,
912 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
913 self.pre_tool_use_hooks()
914 }
915
916 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
924 vec![]
925 }
926
927 fn post_tool_exec_hooks_with_config(
932 &self,
933 _config: &serde_json::Value,
934 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
935 self.post_tool_exec_hooks()
936 }
937
938 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
947 vec![]
948 }
949
950 fn tool_definition_hooks_with_config(
955 &self,
956 _config: &serde_json::Value,
957 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
958 self.tool_definition_hooks()
959 }
960
961 fn tool_definition_hooks_with_context(
971 &self,
972 _ctx: &SystemPromptContext,
973 config: &serde_json::Value,
974 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
975 self.tool_definition_hooks_with_config(config)
976 }
977
978 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
986 vec![]
987 }
988
989 fn narrate(
1003 &self,
1004 _tool_def: Option<&ToolDefinition>,
1005 tool_call: &ToolCall,
1006 phase: crate::tool_narration::ToolNarrationPhase,
1007 locale: Option<&str>,
1008 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1009 ) -> Option<String> {
1010 self.tools()
1011 .iter()
1012 .find(|tool| tool.name() == tool_call.name)
1013 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1014 }
1015
1016 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
1032 vec![]
1033 }
1034
1035 fn user_hooks_with_config(
1041 &self,
1042 _config: &serde_json::Value,
1043 ) -> Vec<crate::user_hook_types::UserHookSpec> {
1044 self.user_hooks()
1045 }
1046
1047 fn risk_level(&self) -> RiskLevel {
1055 RiskLevel::Low
1056 }
1057
1058 fn commands(&self) -> Vec<CommandDescriptor> {
1066 vec![]
1067 }
1068
1069 async fn execute_command(
1083 &self,
1084 request: &ExecuteCommandRequest,
1085 _ctx: &CommandExecutionContext,
1086 ) -> crate::error::Result<CommandResult> {
1087 Err(crate::error::AgentLoopError::config(format!(
1088 "capability {} declared command /{} but does not implement execute_command",
1089 self.id(),
1090 request.name,
1091 )))
1092 }
1093
1094 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1103 vec![]
1104 }
1105
1106 fn contribute_skills(&self) -> Vec<SkillContribution> {
1116 vec![]
1117 }
1118
1119 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1130 vec![]
1131 }
1132
1133 fn post_output_guardrails_with_config(
1145 &self,
1146 _config: &serde_json::Value,
1147 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1148 vec![]
1149 }
1150
1151 fn post_output_annotation_hooks_with_config(
1167 &self,
1168 _config: &serde_json::Value,
1169 ) -> Vec<Arc<dyn crate::annotation_hook::PostGenerationAnnotationHook>> {
1170 vec![]
1171 }
1172
1173 fn citation_verifier_with_config(
1183 &self,
1184 _config: &serde_json::Value,
1185 ) -> Option<Arc<dyn crate::annotation_hook::CitationVerifier>> {
1186 None
1187 }
1188}
1189
1190pub trait ToolDefinitionHook: Send + Sync {
1191 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1192
1193 fn applies_with_native_tool_search(&self) -> bool {
1198 true
1199 }
1200}
1201
1202pub trait ToolCallHook: Send + Sync {
1203 fn narration(
1204 &self,
1205 _tool_def: Option<&ToolDefinition>,
1206 _tool_call: &ToolCall,
1207 _phase: crate::tool_narration::ToolNarrationPhase,
1208 _locale: Option<&str>,
1209 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
1210 ) -> Option<String> {
1211 None
1212 }
1213
1214 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1215 tool_call
1216 }
1217}
1218
1219pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1225
1226impl ToolCallHook for CapabilityNarrationHook {
1227 fn narration(
1228 &self,
1229 tool_def: Option<&ToolDefinition>,
1230 tool_call: &ToolCall,
1231 phase: crate::tool_narration::ToolNarrationPhase,
1232 locale: Option<&str>,
1233 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1234 ) -> Option<String> {
1235 self.0.narrate(tool_def, tool_call, phase, locale, ctx)
1236 }
1237}
1238
1239#[derive(
1243 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1244)]
1245#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1246#[cfg_attr(feature = "openapi", schema(example = "low"))]
1247#[serde(rename_all = "lowercase")]
1248pub enum RiskLevel {
1249 Low,
1251 Medium,
1253 High,
1255}
1256
1257#[derive(Debug, Clone, Serialize, Deserialize)]
1263#[serde(rename_all = "snake_case")]
1264pub enum BlueprintModel {
1265 Fixed(String),
1267 Default(String),
1269 Inherit,
1271}
1272
1273pub struct AgentBlueprint {
1279 pub id: &'static str,
1281 pub name: &'static str,
1283 pub description: &'static str,
1285 pub model: BlueprintModel,
1287 pub system_prompt: &'static str,
1289 pub tools: Vec<Box<dyn Tool>>,
1291 pub max_turns: Option<usize>,
1293 pub config_schema: Option<serde_json::Value>,
1295}
1296
1297impl AgentBlueprint {
1298 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1300 self.tools.iter().map(|t| t.to_definition()).collect()
1301 }
1302}
1303
1304impl std::fmt::Debug for AgentBlueprint {
1305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1306 f.debug_struct("AgentBlueprint")
1307 .field("id", &self.id)
1308 .field("name", &self.name)
1309 .field("model", &self.model)
1310 .field("tool_count", &self.tools.len())
1311 .field("max_turns", &self.max_turns)
1312 .finish()
1313 }
1314}
1315
1316#[derive(Clone)]
1343pub struct CapabilityRegistry {
1344 capabilities: HashMap<String, Arc<dyn Capability>>,
1345 aliases: HashMap<String, String>,
1347}
1348
1349impl CapabilityRegistry {
1350 pub fn new() -> Self {
1352 Self {
1353 capabilities: HashMap::new(),
1354 aliases: HashMap::new(),
1355 }
1356 }
1357
1358 pub fn with_builtins() -> Self {
1363 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1364 }
1365
1366 pub fn runtime_builtins() -> Self {
1377 let mut registry = Self::new();
1378
1379 registry.register(AgentInstructionsCapability);
1380 registry.register(HumanIntentCapability);
1381 registry.register(NoopCapability);
1382 registry.register(CurrentTimeCapability);
1383 registry.register(MessageMetadataCapability);
1384 registry.register(FileSystemCapability);
1385 registry.register(SessionStorageCapability);
1386 registry.register(SessionCapability);
1387 registry.register(StatelessTodoListCapability);
1388 #[cfg(feature = "web-fetch")]
1389 registry.register(WebFetchCapability::from_env());
1390 registry.register(BashkitShellCapability);
1391 registry.register(BtwCapability);
1392 registry.register(InfinityContextCapability);
1393 registry.register(budgeting::BudgetingCapability);
1394 registry.register(SelfBudgetCapability);
1395 registry.register(CompactionCapability);
1396 registry.register(ErrorDisclosureCapability);
1397 registry.register(OpenAiToolSearchCapability::new());
1398 registry.register(ClaudeToolSearchCapability::new());
1399 registry.register(ToolSearchCapability::new());
1400 registry.register(AutoToolSearchCapability::new());
1401 registry.register(PromptCachingCapability::new());
1402 registry.register(ParallelToolCallsCapability);
1403 registry.register(SkillsCapability);
1404 registry.register(SystemCommandsCapability);
1405 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1406 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1407 registry.register(LoopDetectionCapability);
1408 registry.register(ProgressGuardCapability::new());
1409 registry.register(ToolCallRepairCapability);
1410 registry.register(PromptCanaryGuardrailCapability);
1411 registry.register(GuardrailsCapability);
1412 registry.register(user_hooks::UserHooksCapability);
1413
1414 let internal_flags = crate::InternalFeatureFlags::from_env();
1415 if internal_flags.lua {
1416 registry.register(LuaCapability);
1417 registry.register(LuaCodeModeCapability);
1418 }
1419
1420 registry
1421 }
1422
1423 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1428 let mut registry = Self::new();
1429
1430 registry.register(AgentInstructionsCapability);
1432 registry.register(HumanIntentCapability);
1433 registry.register(NoopCapability);
1434 registry.register(CurrentTimeCapability);
1435 registry.register(MessageMetadataCapability);
1436 registry.register(ResearchCapability);
1437 registry.register(ModelScoutCapability);
1438 registry.register(OpenRouterWorkspaceCapability);
1439 registry.register(OpenRouterServerToolsCapability);
1440 registry.register(PlatformManagementCapability);
1441 registry.register(FileSystemCapability);
1442 registry.register(MemoryCapability);
1443 registry.register(SessionStorageCapability);
1444 registry.register(SessionCapability);
1445 registry.register(SessionSqlDatabaseCapability);
1446 registry.register(TestMathCapability);
1447 registry.register(TestWeatherCapability);
1448 registry.register(StatelessTodoListCapability);
1449 #[cfg(feature = "web-fetch")]
1450 registry.register(WebFetchCapability::from_env());
1451 registry.register(BashkitShellCapability);
1452 registry.register(BackgroundExecutionCapability);
1453 registry.register(SessionScheduleCapability);
1454 registry.register(BtwCapability);
1455 registry.register(InfinityContextCapability);
1456 registry.register(budgeting::BudgetingCapability);
1457 registry.register(SelfBudgetCapability);
1458 registry.register(CompactionCapability);
1459 registry.register(ErrorDisclosureCapability);
1460
1461 registry.register(OpenAiToolSearchCapability::new());
1463 registry.register(ClaudeToolSearchCapability::new());
1465 registry.register(ToolSearchCapability::new());
1467 registry.register(AutoToolSearchCapability::new());
1469 registry.register(PromptCachingCapability::new());
1470
1471 registry.register(ParallelToolCallsCapability);
1473
1474 registry.register(SkillsCapability);
1476
1477 registry.register(SubagentCapability);
1479
1480 registry.register(SessionTasksCapability);
1482
1483 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1487 registry.register(AgentHandoffCapability);
1488 #[cfg(feature = "a2a")]
1492 registry.register(A2aAgentDelegationCapability);
1493 }
1494
1495 registry.register(SystemCommandsCapability);
1497
1498 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1500 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1501
1502 registry.register(user_hooks::UserHooksCapability);
1505
1506 registry.register(LoopDetectionCapability);
1508
1509 registry.register(ProgressGuardCapability::new());
1513
1514 registry.register(UsageLimitAutoContinueCapability);
1521
1522 registry.register(ToolCallRepairCapability);
1526
1527 registry.register(PromptCanaryGuardrailCapability);
1530
1531 registry.register(GuardrailsCapability);
1534
1535 #[cfg(feature = "ui-capabilities")]
1537 {
1538 registry.register(OpenUiCapability);
1539 registry.register(A2UiCapability);
1540 }
1541
1542 registry.register(SampleDataCapability);
1544
1545 registry.register(DataKnowledgeCapability);
1547
1548 registry.register(KnowledgeBaseCapability);
1550
1551 registry.register(KnowledgeIndexCapability);
1553
1554 registry.register(CitationRetrievalCapability);
1556
1557 registry.register(CitationVerificationCapability);
1559
1560 registry.register(FakeWarehouseCapability);
1562 registry.register(FakeAwsCapability);
1563 registry.register(FakeCrmCapability);
1564 registry.register(FakeFinancialCapability);
1565
1566 let internal_flags = crate::InternalFeatureFlags::from_env();
1568 if internal_flags.session_sandbox {
1569 registry.register(SessionSandboxCapability);
1570 }
1571
1572 if internal_flags.lua {
1576 registry.register(LuaCapability);
1577 registry.register(LuaCodeModeCapability);
1580 }
1581 for plugin in inventory::iter::<IntegrationPlugin>() {
1582 if (!plugin.experimental_only || grade.experimental_features_enabled())
1583 && plugin
1584 .feature_flag
1585 .is_none_or(|f| internal_flags.is_enabled(f))
1586 {
1587 registry.register_boxed((plugin.factory)());
1588 }
1589 }
1590
1591 registry
1592 }
1593
1594 pub fn register(&mut self, capability: impl Capability + 'static) {
1596 self.register_arc(Arc::new(capability));
1597 }
1598
1599 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1601 self.register_arc(Arc::from(capability));
1602 }
1603
1604 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1606 let canonical = capability.id().to_string();
1607 for alias in capability.aliases() {
1608 self.aliases.insert(alias.to_string(), canonical.clone());
1609 }
1610 self.capabilities.insert(canonical, capability);
1611 }
1612
1613 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1615 self.capabilities
1616 .get(id)
1617 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1618 }
1619
1620 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1625 if self.capabilities.contains_key(id) {
1626 Some(id)
1627 } else {
1628 self.aliases
1629 .get(id)
1630 .filter(|c| self.capabilities.contains_key(*c))
1631 .map(String::as_str)
1632 }
1633 }
1634
1635 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1637 let canonical = self.canonical_id(id)?.to_string();
1638 let removed = self.capabilities.remove(&canonical);
1639 self.aliases.retain(|_, target| *target != canonical);
1640 removed
1641 }
1642
1643 pub fn has(&self, id: &str) -> bool {
1645 self.get(id).is_some()
1646 }
1647
1648 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1650 self.capabilities.values().collect()
1651 }
1652
1653 pub fn len(&self) -> usize {
1655 self.capabilities.len()
1656 }
1657
1658 pub fn is_empty(&self) -> bool {
1660 self.capabilities.is_empty()
1661 }
1662
1663 pub fn builder() -> CapabilityRegistryBuilder {
1665 CapabilityRegistryBuilder::new()
1666 }
1667
1668 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1672 for cap in self.capabilities.values() {
1673 for bp in cap.agent_blueprints() {
1674 if bp.id == id {
1675 return Some(bp);
1676 }
1677 }
1678 }
1679 None
1680 }
1681
1682 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1686 for (capability_id, cap) in &self.capabilities {
1687 for bp in cap.agent_blueprints() {
1688 if bp.id == id {
1689 return Some((capability_id.clone(), bp));
1690 }
1691 }
1692 }
1693 None
1694 }
1695
1696 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1698 self.capabilities
1699 .values()
1700 .flat_map(|cap| cap.agent_blueprints())
1701 .collect()
1702 }
1703}
1704
1705impl Default for CapabilityRegistry {
1706 fn default() -> Self {
1707 Self::with_builtins()
1708 }
1709}
1710
1711impl std::fmt::Debug for CapabilityRegistry {
1712 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1713 let ids: Vec<_> = self.capabilities.keys().collect();
1714 f.debug_struct("CapabilityRegistry")
1715 .field("capabilities", &ids)
1716 .finish()
1717 }
1718}
1719
1720pub struct CapabilityRegistryBuilder {
1722 registry: CapabilityRegistry,
1723}
1724
1725impl CapabilityRegistryBuilder {
1726 pub fn new() -> Self {
1728 Self {
1729 registry: CapabilityRegistry::new(),
1730 }
1731 }
1732
1733 pub fn with_builtins() -> Self {
1735 Self {
1736 registry: CapabilityRegistry::with_builtins(),
1737 }
1738 }
1739
1740 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1742 self.registry.register(capability);
1743 self
1744 }
1745
1746 pub fn build(self) -> CapabilityRegistry {
1748 self.registry
1749 }
1750}
1751
1752impl Default for CapabilityRegistryBuilder {
1753 fn default() -> Self {
1754 Self::new()
1755 }
1756}
1757
1758pub struct ModelViewContext<'a> {
1764 pub session_id: SessionId,
1765 pub prior_usage: Option<&'a TokenUsage>,
1766}
1767
1768pub trait ModelViewProvider: Send + Sync {
1774 fn apply_model_view(
1775 &self,
1776 messages: Vec<Message>,
1777 config: &serde_json::Value,
1778 context: &ModelViewContext<'_>,
1779 ) -> Vec<Message>;
1780
1781 fn priority(&self) -> i32 {
1782 0
1783 }
1784}
1785
1786pub struct CollectedCapabilities {
1791 pub system_prompt_parts: Vec<String>,
1793 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1795 pub tools: Vec<Box<dyn Tool>>,
1797 pub tool_definitions: Vec<ToolDefinition>,
1799 pub mounts: Vec<MountPoint>,
1801 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1803 pub applied_ids: Vec<String>,
1805 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1807 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1809 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1812 pub parallel_tool_calls: Option<bool>,
1816 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1818 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1820 pub mcp_servers: ScopedMcpServers,
1822 }
1828
1829#[derive(Debug, Clone, PartialEq, Eq)]
1830pub struct SystemPromptAttribution {
1831 pub capability_id: String,
1832 pub content: String,
1833}
1834
1835impl CollectedCapabilities {
1836 pub fn system_prompt_prefix(&self) -> Option<String> {
1839 if self.system_prompt_parts.is_empty() {
1840 None
1841 } else {
1842 Some(self.system_prompt_parts.join("\n\n"))
1843 }
1844 }
1845
1846 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1850 for (provider, config) in &self.message_filter_providers {
1852 provider.apply_filters(query, config);
1853 }
1854 }
1855
1856 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1859 for (provider, config) in &self.message_filter_providers {
1860 provider.post_load(messages, config);
1861 }
1862 }
1863
1864 pub fn has_message_filters(&self) -> bool {
1866 !self.message_filter_providers.is_empty()
1867 }
1868}
1869
1870struct SpawnAgentTargetProvider {
1871 target_type: &'static str,
1872 tool: Box<dyn Tool>,
1873}
1874
1875#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1877#[serde(rename_all = "snake_case")]
1878pub(crate) enum SpawnMode {
1879 Background,
1880 Foreground,
1881}
1882
1883impl SpawnMode {
1884 pub(crate) fn parse(value: &str) -> Option<Self> {
1885 match value {
1886 "background" => Some(Self::Background),
1887 "foreground" => Some(Self::Foreground),
1888 _ => None,
1889 }
1890 }
1891
1892 pub(crate) fn as_str(self) -> &'static str {
1893 match self {
1894 Self::Background => "background",
1895 Self::Foreground => "foreground",
1896 }
1897 }
1898}
1899
1900struct UnifiedSpawnAgentTool {
1901 providers: Vec<SpawnAgentTargetProvider>,
1902}
1903
1904impl UnifiedSpawnAgentTool {
1905 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1906 Self { providers }
1907 }
1908
1909 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1910 self.providers
1911 .iter()
1912 .find(|provider| provider.target_type == target_type)
1913 .map(|provider| provider.tool.as_ref())
1914 }
1915
1916 fn target_types(&self) -> Vec<&'static str> {
1917 ["subagent", "agent", "external_a2a"]
1918 .into_iter()
1919 .filter(|target_type| {
1920 self.providers
1921 .iter()
1922 .any(|provider| provider.target_type == *target_type)
1923 })
1924 .collect()
1925 }
1926
1927 fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
1932 self.target_types()
1933 .into_iter()
1934 .filter_map(|target_type| match target_type {
1935 "subagent" => Some(serde_json::json!({
1936 "properties": {
1937 "type": {"const": "subagent"}
1938 }
1939 })),
1940 "agent" => Some(serde_json::json!({
1941 "properties": {
1942 "type": {"const": "agent"}
1943 },
1944 "required": ["type", "id"]
1945 })),
1946 "external_a2a" => Some(serde_json::json!({
1947 "properties": {
1948 "type": {"const": "external_a2a"}
1949 },
1950 "anyOf": [
1951 {"required": ["id"]},
1952 {"required": ["external_agent_id"]}
1953 ]
1954 })),
1955 _ => None,
1956 })
1957 .collect()
1958 }
1959
1960 }
1970
1971#[async_trait]
1972impl Tool for UnifiedSpawnAgentTool {
1973 fn narrate(
1974 &self,
1975 tool_call: &ToolCall,
1976 phase: crate::tool_narration::ToolNarrationPhase,
1977 locale: Option<&str>,
1978 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1979 ) -> Option<String> {
1980 let target_type = tool_call
1981 .arguments
1982 .get("target")
1983 .and_then(|target| target.get("type"))
1984 .and_then(serde_json::Value::as_str)?;
1985 self.provider_for(target_type)
1986 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1987 }
1988
1989 fn name(&self) -> &str {
1990 "spawn_agent"
1991 }
1992
1993 fn display_name(&self) -> Option<&str> {
1994 Some("Spawn Agent")
1995 }
1996
1997 fn description(&self) -> &str {
1998 "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."
1999 }
2000
2001 fn parameters_schema(&self) -> serde_json::Value {
2002 serde_json::json!({
2003 "type": "object",
2004 "properties": {
2005 "name": {
2006 "type": "string",
2007 "description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
2008 },
2009 "instructions": {
2010 "type": "string",
2011 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
2012 },
2013 "goal": {
2014 "type": "string",
2015 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
2016 },
2017 "lifetime": {
2018 "type": "string",
2019 "enum": ["linked", "detached"],
2020 "default": "linked",
2021 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
2022 },
2023 "seed": {
2024 "type": "string",
2025 "enum": ["fresh", "fork", "workspace"],
2026 "default": "fresh",
2027 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
2028 },
2029 "target": {
2030 "type": "object",
2031 "properties": {
2032 "type": {
2033 "type": "string",
2034 "enum": self.target_types(),
2035 "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."
2036 },
2037 "id": {
2038 "type": "string",
2039 "description": "Configured target id for first-party handoffs or external A2A agents."
2040 },
2041 "external_agent_id": {
2042 "type": "string",
2043 "description": "Configured external A2A agent id."
2044 }
2045 },
2046 "required": ["type"],
2047 "oneOf": self.target_constraint_branches(),
2048 "additionalProperties": false
2049 },
2050 "mode": {
2051 "type": "string",
2052 "enum": ["background", "foreground"],
2053 "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."
2054 },
2055 "blueprint": {
2056 "type": "string",
2057 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
2058 },
2059 "config": {
2060 "type": "object",
2061 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
2062 },
2063 "result_schema": {
2064 "type": "object",
2065 "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."
2066 },
2067 "message_schema": {
2068 "type": "object",
2069 "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."
2070 },
2071 "public_context": {
2072 "type": "object",
2073 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
2074 },
2075 "wait_timeout_secs": {
2076 "type": "integer",
2077 "minimum": 1,
2078 "maximum": 86400,
2079 "description": "External-A2A-only foreground timeout."
2080 },
2081 "wake_on_completion": {
2082 "type": "boolean",
2083 "description": "External-A2A-only control for background completion wake-ups."
2084 }
2085 },
2086 "required": ["name", "instructions", "target"],
2087 "additionalProperties": false
2088 })
2089 }
2090
2091 fn hints(&self) -> crate::tool_types::ToolHints {
2092 let mut hints = crate::tool_types::ToolHints::default()
2093 .with_long_running(true)
2094 .with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
2095 if self.provider_for("external_a2a").is_some() {
2096 hints = hints.with_open_world(true);
2097 }
2098 hints
2099 }
2100
2101 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
2102 ToolExecutionResult::tool_error(
2103 "spawn_agent requires context. This tool must be executed with session context.",
2104 )
2105 }
2106
2107 async fn execute_with_context(
2108 &self,
2109 arguments: serde_json::Value,
2110 context: &ToolContext,
2111 ) -> ToolExecutionResult {
2112 let target_type = match arguments
2113 .get("target")
2114 .and_then(|target| target.get("type"))
2115 .and_then(serde_json::Value::as_str)
2116 {
2117 Some(target_type) => target_type,
2118 None => {
2119 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
2120 }
2121 };
2122
2123 let Some(provider) = self.provider_for(target_type) else {
2124 let supported = self.target_types().join(", ");
2125 return ToolExecutionResult::tool_error(format!(
2126 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
2127 ));
2128 };
2129 if target_type == "external_a2a"
2130 && arguments
2131 .get("lifetime")
2132 .and_then(serde_json::Value::as_str)
2133 .is_some_and(|value| value == "detached")
2134 {
2135 return ToolExecutionResult::tool_error(
2136 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2137 );
2138 }
2139 if target_type == "external_a2a"
2140 && arguments
2141 .get("message_schema")
2142 .is_some_and(|schema| !schema.is_null())
2143 {
2144 return ToolExecutionResult::tool_error(
2145 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2146 );
2147 }
2148
2149 provider.execute_with_context(arguments, context).await
2150 }
2151
2152 fn requires_context(&self) -> bool {
2153 true
2154 }
2155}
2156
2157pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2162 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2163 return base_system_prompt.to_string();
2164 };
2165
2166 if base_system_prompt.is_empty() {
2167 return additions.to_string();
2168 }
2169
2170 if base_system_prompt.contains("<system-prompt>") {
2171 format!("{base_system_prompt}\n\n{additions}")
2172 } else {
2173 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2174 }
2175}
2176
2177pub struct CollectedMessageFilters {
2184 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2186}
2187
2188pub struct CollectedModelViewProviders {
2190 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2192}
2193
2194impl CollectedMessageFilters {
2200 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2202 for (provider, config) in &self.message_filter_providers {
2203 provider.apply_filters(query, config);
2204 }
2205 }
2206
2207 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2209 for (provider, config) in &self.message_filter_providers {
2210 provider.post_load(messages, config);
2211 }
2212 }
2213}
2214
2215impl CollectedModelViewProviders {
2216 pub fn apply_model_view(
2218 &self,
2219 mut messages: Vec<Message>,
2220 context: &ModelViewContext<'_>,
2221 ) -> Vec<Message> {
2222 for (provider, config) in &self.model_view_providers {
2223 messages = provider.apply_model_view(messages, config, context);
2224 }
2225 messages
2226 }
2227}
2228
2229fn compaction_is_enabled(
2235 capability_configs: &[AgentCapabilityConfig],
2236 registry: &CapabilityRegistry,
2237) -> bool {
2238 capability_configs.iter().any(|cap_config| {
2239 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2240 && registry
2241 .get(cap_config.capability_ref.as_str())
2242 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2243 })
2244}
2245
2246fn message_filter_config_for(
2255 cap_id: &str,
2256 base: &serde_json::Value,
2257 compaction_on: bool,
2258) -> serde_json::Value {
2259 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2260 return base.clone();
2261 }
2262 let mut config = base.clone();
2263 match config.as_object_mut() {
2264 Some(map) => {
2265 map.insert(
2266 "compaction_active".to_string(),
2267 serde_json::Value::Bool(true),
2268 );
2269 }
2270 None => {
2271 config = serde_json::json!({ "compaction_active": true });
2272 }
2273 }
2274 config
2275}
2276
2277pub fn collect_message_filters_only(
2283 capability_configs: &[AgentCapabilityConfig],
2284 registry: &CapabilityRegistry,
2285) -> CollectedMessageFilters {
2286 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2287 Vec::new();
2288 let compaction_on = compaction_is_enabled(capability_configs, registry);
2289
2290 for cap_config in capability_configs {
2291 let cap_id = cap_config.capability_ref.as_str();
2292 if let Some(capability) = registry.get(cap_id) {
2293 if capability.status() != CapabilityStatus::Available {
2294 continue;
2295 }
2296 let effective: &dyn Capability = capability
2299 .resolve_for_model(None)
2300 .unwrap_or_else(|| capability.as_ref());
2301 if let Some(provider) = effective.message_filter_provider() {
2302 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2303 message_filter_providers.push((provider, config));
2304 }
2305 }
2306 }
2307
2308 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2309
2310 CollectedMessageFilters {
2311 message_filter_providers,
2312 }
2313}
2314
2315pub fn collect_model_view_providers(
2322 capability_configs: &[AgentCapabilityConfig],
2323 registry: &CapabilityRegistry,
2324 model: Option<&str>,
2325) -> CollectedModelViewProviders {
2326 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2327
2328 for cap_config in capability_configs {
2329 let cap_id = cap_config.capability_ref.as_str();
2330 if let Some(capability) = registry.get(cap_id) {
2331 if capability.status() != CapabilityStatus::Available {
2332 continue;
2333 }
2334 let effective: &dyn Capability = capability
2335 .resolve_for_model(model)
2336 .unwrap_or_else(|| capability.as_ref());
2337 if let Some(provider) = effective.model_view_provider() {
2338 model_view_providers.push((provider, cap_config.config.clone()));
2339 }
2340 }
2341 }
2342
2343 model_view_providers.sort_by_key(|(p, _)| p.priority());
2344
2345 CollectedModelViewProviders {
2346 model_view_providers,
2347 }
2348}
2349
2350pub fn collect_dynamic_facts(
2356 capability_configs: &[AgentCapabilityConfig],
2357 registry: &CapabilityRegistry,
2358 model: Option<&str>,
2359 ctx: &FactsContext,
2360) -> Vec<Fact> {
2361 let mut dynamic = Vec::new();
2362 for cap_config in capability_configs {
2363 let cap_id = cap_config.capability_ref.as_str();
2364 if let Some(capability) = registry.get(cap_id) {
2365 if capability.status() != CapabilityStatus::Available {
2366 continue;
2367 }
2368 let effective: &dyn Capability = capability
2369 .resolve_for_model(model)
2370 .unwrap_or_else(|| capability.as_ref());
2371 for fact in effective.facts(&cap_config.config, ctx) {
2372 if fact.volatility == Volatility::Dynamic {
2373 dynamic.push(fact);
2374 }
2375 }
2376 }
2377 }
2378 dynamic
2379}
2380
2381pub fn collect_capability_mcp_servers(
2382 capability_configs: &[AgentCapabilityConfig],
2383 registry: &CapabilityRegistry,
2384) -> ScopedMcpServers {
2385 let mut servers = ScopedMcpServers::default();
2386
2387 for cap_config in capability_configs {
2388 let cap_id = cap_config.capability_ref.as_str();
2389 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2392 if let Ok(definition) =
2393 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2394 {
2395 if definition.status != CapabilityStatus::Available {
2396 continue;
2397 }
2398 if let Some(contributed) = definition.mcp_servers {
2399 servers = merge_scoped_mcp_servers(&servers, &contributed);
2400 }
2401 }
2402 continue;
2403 }
2404 if let Some(capability) = registry.get(cap_id) {
2405 if capability.status() != CapabilityStatus::Available {
2406 continue;
2407 }
2408 servers = merge_scoped_mcp_servers(
2409 &servers,
2410 &capability.mcp_servers_with_config(&cap_config.config),
2411 );
2412 }
2413 }
2414
2415 servers
2416}
2417
2418pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2425
2426#[derive(Debug, Clone, PartialEq, Eq)]
2428pub enum DependencyError {
2429 CircularDependency {
2431 capability_id: String,
2433 chain: Vec<String>,
2435 },
2436 TooManyCapabilities {
2438 count: usize,
2440 max: usize,
2442 },
2443}
2444
2445impl std::fmt::Display for DependencyError {
2446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2447 match self {
2448 DependencyError::CircularDependency {
2449 capability_id,
2450 chain,
2451 } => {
2452 write!(
2453 f,
2454 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2455 capability_id,
2456 chain.join(" -> "),
2457 capability_id
2458 )
2459 }
2460 DependencyError::TooManyCapabilities { count, max } => {
2461 write!(
2462 f,
2463 "Too many capabilities after resolution: {} (max: {})",
2464 count, max
2465 )
2466 }
2467 }
2468 }
2469}
2470
2471impl std::error::Error for DependencyError {}
2472
2473#[derive(Debug, Clone)]
2475pub struct ResolvedCapabilities {
2476 pub resolved_ids: Vec<String>,
2479 pub added_as_dependencies: Vec<String>,
2481 pub user_selected: Vec<String>,
2483}
2484
2485pub fn resolve_dependencies(
2505 selected_ids: &[String],
2506 registry: &CapabilityRegistry,
2507) -> Result<ResolvedCapabilities, DependencyError> {
2508 use std::collections::HashSet;
2509
2510 let user_selected: HashSet<String> = selected_ids
2512 .iter()
2513 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2514 .collect();
2515 let mut resolved: Vec<String> = Vec::new();
2516 let mut resolved_set: HashSet<String> = HashSet::new();
2517 let mut added_as_dependencies: Vec<String> = Vec::new();
2518
2519 for cap_id in selected_ids {
2521 resolve_single_capability(
2522 cap_id,
2523 registry,
2524 &mut resolved,
2525 &mut resolved_set,
2526 &mut added_as_dependencies,
2527 &user_selected,
2528 &mut Vec::new(), )?;
2530 }
2531
2532 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2534 return Err(DependencyError::TooManyCapabilities {
2535 count: resolved.len(),
2536 max: MAX_RESOLVED_CAPABILITIES,
2537 });
2538 }
2539
2540 Ok(ResolvedCapabilities {
2541 resolved_ids: resolved,
2542 added_as_dependencies,
2543 user_selected: selected_ids.to_vec(),
2544 })
2545}
2546
2547pub fn resolve_capability_configs(
2552 selected_configs: &[AgentCapabilityConfig],
2553 registry: &CapabilityRegistry,
2554) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2555 let mut selected_ids: Vec<String> = Vec::new();
2556 for config in selected_configs {
2557 if (is_declarative_capability(config.capability_id())
2560 || is_plugin_capability(config.capability_id()))
2561 && let Ok(definition) =
2562 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2563 {
2564 selected_ids.extend(definition.dependencies);
2565 }
2566 selected_ids.push(config.capability_id().to_string());
2567 }
2568 let resolved = resolve_dependencies(&selected_ids, registry)?;
2569
2570 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2573 .iter()
2574 .map(|config| {
2575 let id = config.capability_id();
2576 let id = registry.canonical_id(id).unwrap_or(id);
2577 (id.to_string(), config.config.clone())
2578 })
2579 .collect();
2580
2581 Ok(resolved
2582 .resolved_ids
2583 .into_iter()
2584 .map(|capability_id| {
2585 explicit_configs
2586 .get(&capability_id)
2587 .cloned()
2588 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2589 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2590 })
2591 .collect())
2592}
2593
2594fn resolve_single_capability(
2596 cap_id: &str,
2597 registry: &CapabilityRegistry,
2598 resolved: &mut Vec<String>,
2599 resolved_set: &mut std::collections::HashSet<String>,
2600 added_as_dependencies: &mut Vec<String>,
2601 user_selected: &std::collections::HashSet<String>,
2602 visiting: &mut Vec<String>,
2603) -> Result<(), DependencyError> {
2604 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2608
2609 if resolved_set.contains(cap_id) {
2611 return Ok(());
2612 }
2613
2614 if visiting.contains(&cap_id.to_string()) {
2616 return Err(DependencyError::CircularDependency {
2617 capability_id: cap_id.to_string(),
2618 chain: visiting.clone(),
2619 });
2620 }
2621
2622 let capability = match registry.get(cap_id) {
2624 Some(cap) => cap,
2625 None => {
2626 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2630 && !resolved_set.contains(cap_id)
2631 {
2632 resolved.push(cap_id.to_string());
2633 resolved_set.insert(cap_id.to_string());
2634 if !user_selected.contains(cap_id) {
2635 added_as_dependencies.push(cap_id.to_string());
2636 }
2637 }
2638 return Ok(());
2639 }
2640 };
2641
2642 visiting.push(cap_id.to_string());
2644
2645 for dep_id in capability.dependencies() {
2647 resolve_single_capability(
2648 dep_id,
2649 registry,
2650 resolved,
2651 resolved_set,
2652 added_as_dependencies,
2653 user_selected,
2654 visiting,
2655 )?;
2656 }
2657
2658 visiting.pop();
2660
2661 if !resolved_set.contains(cap_id) {
2663 resolved.push(cap_id.to_string());
2664 resolved_set.insert(cap_id.to_string());
2665
2666 if !user_selected.contains(cap_id) {
2668 added_as_dependencies.push(cap_id.to_string());
2669 }
2670 }
2671
2672 Ok(())
2673}
2674
2675pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2680 use std::collections::HashSet;
2681
2682 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2683 Ok(resolved) => resolved.resolved_ids,
2684 Err(_) => capability_ids.to_vec(),
2685 };
2686
2687 let mut seen = HashSet::new();
2688 let mut features = Vec::new();
2689 for cap_id in &resolved_ids {
2690 if let Some(cap) = registry.get(cap_id) {
2691 for feature in cap.features() {
2692 if seen.insert(feature) {
2693 features.push(feature.to_string());
2694 }
2695 }
2696 }
2697 }
2698 features
2699}
2700
2701pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2704 registry
2705 .get(cap_id)
2706 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2707 .unwrap_or_default()
2708}
2709
2710pub async fn collect_capabilities(
2726 capability_ids: &[String],
2727 registry: &CapabilityRegistry,
2728 ctx: &SystemPromptContext,
2729) -> CollectedCapabilities {
2730 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2733 Ok(resolved) => resolved.resolved_ids,
2734 Err(e) => {
2735 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2736 capability_ids.to_vec()
2737 }
2738 };
2739
2740 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2742 .iter()
2743 .map(|id| AgentCapabilityConfig {
2744 capability_ref: CapabilityId::new(id),
2745 config: serde_json::Value::Object(serde_json::Map::new()),
2746 })
2747 .collect();
2748
2749 collect_capabilities_with_configs(&configs, registry, ctx).await
2750}
2751
2752pub async fn collect_capabilities_with_configs(
2763 capability_configs: &[AgentCapabilityConfig],
2764 registry: &CapabilityRegistry,
2765 ctx: &SystemPromptContext,
2766) -> CollectedCapabilities {
2767 let mut system_prompt_parts: Vec<String> = Vec::new();
2768 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2769 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2770 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2771 let mut mounts: Vec<MountPoint> = Vec::new();
2772 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2773 Vec::new();
2774 let mut applied_ids: Vec<String> = Vec::new();
2775 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2776 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2777 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2778 let mut parallel_tool_calls: Option<bool> = None;
2779 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2780 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2781 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2784 let mut mcp_servers = ScopedMcpServers::default();
2785 let mut static_facts: Vec<Fact> = Vec::new();
2789 let mut has_dynamic_facts = false;
2790 let facts_ctx = FactsContext::new(ctx.session_id);
2791 let compaction_on = compaction_is_enabled(capability_configs, registry);
2792 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2793 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2794
2795 for cap_config in capability_configs {
2796 let cap_id = cap_config.capability_ref.as_str();
2797 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2802 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2803 cap_config.config.clone(),
2804 ) {
2805 Ok(definition) => {
2806 if definition.status != CapabilityStatus::Available {
2807 continue;
2808 }
2809
2810 if let Some(prompt) = definition.system_prompt.as_deref() {
2811 let contribution =
2812 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2813 system_prompt_attributions.push(SystemPromptAttribution {
2814 capability_id: cap_id.to_string(),
2815 content: contribution.clone(),
2816 });
2817 system_prompt_parts.push(contribution);
2818 }
2819
2820 mounts.extend(definition.mounts(cap_id));
2821 if let Some(ref servers) = definition.mcp_servers {
2822 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2823 }
2824 for skill in definition.skill_contributions() {
2825 mounts.push(skill.to_mount(cap_id));
2826 }
2827
2828 applied_ids.push(cap_id.to_string());
2829 }
2830 Err(error) => {
2831 tracing::warn!(
2832 capability_id = %cap_id,
2833 error = %error,
2834 "Skipping invalid declarative/plugin capability config"
2835 );
2836 }
2837 }
2838 continue;
2839 }
2840 if let Some(capability) = registry.get(cap_id) {
2841 if capability.status() != CapabilityStatus::Available {
2843 continue;
2844 }
2845
2846 let effective: &dyn Capability =
2858 match capability.resolve_for_model(ctx.model.as_deref()) {
2859 Some(inner) => inner,
2860 None => capability.as_ref(),
2861 };
2862 let effective_id = effective.id();
2863 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2864 agent_handoff_spawn_config = Some(cap_config.config.clone());
2865 }
2866
2867 if let Some(contribution) = effective
2869 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2870 .await
2871 {
2872 system_prompt_attributions.push(SystemPromptAttribution {
2873 capability_id: cap_id.to_string(),
2874 content: contribution.clone(),
2875 });
2876 system_prompt_parts.push(contribution);
2877 }
2878
2879 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2884 match fact.volatility {
2885 Volatility::Static => static_facts.push(fact),
2886 Volatility::Dynamic => has_dynamic_facts = true,
2887 }
2888 }
2889
2890 for tool in effective.tools_with_config(&cap_config.config) {
2892 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2893 spawn_agent_providers.push(SpawnAgentTargetProvider {
2894 target_type: "external_a2a",
2895 tool,
2896 });
2897 } else {
2898 tools.push(tool);
2899 }
2900 }
2901 tool_definition_hooks
2902 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2903 tool_call_hooks.extend(effective.tool_call_hooks());
2904 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2906 let cap_category = effective.category();
2911 for def in effective.tool_definitions() {
2912 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2913 continue;
2914 }
2915 let def = match (def.category(), cap_category) {
2916 (None, Some(cat)) => def.with_category(cat),
2917 _ => def,
2918 }
2919 .with_capability_attribution(cap_id, Some(capability.name()));
2920 tool_definitions.push(def);
2921 }
2922
2923 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2931 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2932 {
2933 let threshold = cap_config
2935 .config
2936 .get("threshold")
2937 .and_then(|v| v.as_u64())
2938 .map(|v| v as usize)
2939 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2940 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2941 enabled: true,
2942 threshold,
2943 });
2944 }
2945
2946 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2947 let strategy = cap_config
2948 .config
2949 .get("strategy")
2950 .and_then(|v| v.as_str())
2951 .map(|value| match value {
2952 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2953 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2954 })
2955 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2956 let gemini_cached_content = cap_config
2957 .config
2958 .get("gemini_cached_content")
2959 .and_then(|v| v.as_str())
2960 .map(str::to_string);
2961 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2962 enabled: true,
2963 strategy,
2964 gemini_cached_content,
2965 });
2966 }
2967
2968 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2969 parallel_tool_calls =
2970 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2971 }
2972
2973 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2974 let server_tools =
2975 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2976 if !server_tools.is_empty() {
2977 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2978 server_tools,
2979 ..Default::default()
2980 });
2981 }
2982 }
2983
2984 mounts.extend(effective.mounts());
2986
2987 mcp_servers = merge_scoped_mcp_servers(
2988 &mcp_servers,
2989 &effective.mcp_servers_with_config(&cap_config.config),
2990 );
2991
2992 for skill in effective.contribute_skills() {
2996 mounts.push(skill.to_mount(cap_id));
2997 }
2998
2999 if let Some(provider) = effective.message_filter_provider() {
3001 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
3002 message_filter_providers.push((provider, config));
3003 }
3004
3005 applied_ids.push(cap_id.to_string());
3006 }
3007 }
3008
3009 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
3014 spawn_agent_providers.push(SpawnAgentTargetProvider {
3015 target_type: "subagent",
3016 tool: Box::new(SpawnSubagentAsAgentTool),
3017 });
3018 }
3019 if let Some(config) = agent_handoff_spawn_config.as_ref() {
3020 spawn_agent_providers.push(SpawnAgentTargetProvider {
3021 target_type: "agent",
3022 tool: Box::new(SpawnAgentHandoffTool::new(config)),
3023 });
3024 }
3025 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
3026 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
3027 let def = tool
3028 .to_definition()
3029 .with_category("Orchestration")
3030 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
3031 tools.push(Box::new(tool));
3032 tool_definitions.push(def);
3033 }
3034
3035 if !applied_ids
3047 .iter()
3048 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
3049 && tool_definitions
3050 .iter()
3051 .any(|def| def.hints().supports_background == Some(true))
3052 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
3053 && bg_cap.status() == CapabilityStatus::Available
3054 {
3055 tools.extend(bg_cap.tools());
3056 let cap_category = bg_cap.category();
3057 for def in bg_cap.tool_definitions() {
3058 let def = match (def.category(), cap_category) {
3059 (None, Some(cat)) => def.with_category(cat),
3060 _ => def,
3061 }
3062 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
3063 tool_definitions.push(def);
3064 }
3065 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
3066 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
3067 }
3068
3069 if let Some(block) = facts::render_facts_block(&static_facts) {
3074 system_prompt_attributions.push(SystemPromptAttribution {
3075 capability_id: "facts".to_string(),
3076 content: block.clone(),
3077 });
3078 system_prompt_parts.push(block);
3079 }
3080 if has_dynamic_facts {
3081 system_prompt_attributions.push(SystemPromptAttribution {
3082 capability_id: "facts".to_string(),
3083 content: FACTS_DYNAMIC_NOTE.to_string(),
3084 });
3085 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
3086 }
3087
3088 tool_call_hooks.extend(narration_hooks);
3092
3093 message_filter_providers.sort_by_key(|(p, _)| p.priority());
3095
3096 CollectedCapabilities {
3097 system_prompt_parts,
3098 system_prompt_attributions,
3099 tools,
3100 tool_definitions,
3101 mounts,
3102 message_filter_providers,
3103 applied_ids,
3104 tool_search,
3105 prompt_cache,
3106 openrouter_routing,
3107 parallel_tool_calls,
3108 tool_definition_hooks,
3109 tool_call_hooks,
3110 mcp_servers,
3111 }
3112}
3113
3114pub struct AppliedCapabilities {
3120 pub runtime_agent: RuntimeAgent,
3122 pub tool_registry: ToolRegistry,
3124 pub applied_ids: Vec<String>,
3126}
3127
3128pub async fn apply_capabilities(
3165 base_runtime_agent: RuntimeAgent,
3166 capability_ids: &[String],
3167 registry: &CapabilityRegistry,
3168 ctx: &SystemPromptContext,
3169) -> AppliedCapabilities {
3170 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3171
3172 let final_system_prompt = compose_system_prompt(
3174 &base_runtime_agent.system_prompt,
3175 collected.system_prompt_prefix().as_deref(),
3176 );
3177
3178 let mut tool_registry = ToolRegistry::new();
3180 for tool in collected.tools {
3181 tool_registry.register_boxed(tool);
3182 }
3183
3184 let mut tools = collected.tool_definitions;
3186 for hook in &collected.tool_definition_hooks {
3187 tools = hook.transform(tools);
3188 }
3189
3190 let runtime_agent = RuntimeAgent {
3191 system_prompt: final_system_prompt,
3192 model: base_runtime_agent.model,
3193 tools,
3194 max_iterations: base_runtime_agent.max_iterations,
3195 temperature: base_runtime_agent.temperature,
3196 max_tokens: base_runtime_agent.max_tokens,
3197 tool_search: collected.tool_search,
3198 prompt_cache: collected.prompt_cache,
3199 openrouter_routing: collected.openrouter_routing,
3200 network_access: base_runtime_agent.network_access,
3201 parallel_tool_calls: base_runtime_agent
3204 .parallel_tool_calls
3205 .or(collected.parallel_tool_calls),
3206 };
3207
3208 AppliedCapabilities {
3209 runtime_agent,
3210 tool_registry,
3211 applied_ids: collected.applied_ids,
3212 }
3213}
3214
3215#[cfg(test)]
3220mod tests {
3221 use super::*;
3222 use crate::typed_id::SessionId;
3223 use std::collections::BTreeSet;
3224 use uuid::Uuid;
3225
3226 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3228
3229 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3230 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3231 }
3232
3233 fn test_ctx() -> SystemPromptContext {
3235 SystemPromptContext::without_file_store(SessionId::new())
3236 }
3237
3238 struct HostAnnotatedCapability;
3240
3241 #[async_trait]
3242 impl Capability for HostAnnotatedCapability {
3243 fn id(&self) -> &str {
3244 "host_annotated"
3245 }
3246 fn name(&self) -> &str {
3247 "Host Annotated"
3248 }
3249 fn description(&self) -> &str {
3250 "Test capability with host-owned metadata."
3251 }
3252 fn metadata(&self) -> Option<serde_json::Value> {
3253 Some(serde_json::json!({"icon": "sparkles", "group": "host"}))
3254 }
3255 }
3256
3257 #[test]
3258 fn capability_metadata_is_an_opt_in_host_hatch() {
3259 assert!(NoopCapability.metadata().is_none());
3261
3262 let metadata = HostAnnotatedCapability.metadata().expect("metadata");
3263 assert_eq!(metadata["icon"], "sparkles");
3264 assert_eq!(metadata["group"], "host");
3265 }
3266
3267 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3269 let mut ids = [
3270 "agent_instructions",
3271 "human_intent",
3272 "budgeting",
3273 "self_budget",
3274 "noop",
3275 "current_time",
3276 "research",
3277 "platform_management",
3278 "session_file_system",
3279 "session_storage",
3280 "session",
3281 "session_sql_database",
3282 "test_math",
3283 "test_weather",
3284 "stateless_todo_list",
3285 "web_fetch",
3286 "bashkit_shell",
3287 "background_execution",
3288 "session_schedule",
3289 "btw",
3290 "infinity_context",
3291 "compaction",
3292 "memory",
3293 "message_metadata",
3294 "openai_tool_search",
3295 "claude_tool_search",
3296 "tool_search",
3297 "auto_tool_search",
3298 "prompt_caching",
3299 "parallel_tool_calls",
3300 "session_tasks",
3301 "skills",
3302 "subagents",
3303 "system_commands",
3304 "sample_data",
3305 "data_knowledge",
3306 "knowledge_base",
3307 "knowledge_index",
3308 "citation_retrieval",
3309 "citation_verification",
3310 "tool_output_persistence",
3311 "tool_output_distillation",
3312 "fake_warehouse",
3313 "fake_aws",
3314 "fake_crm",
3315 "fake_financial",
3316 "loop_detection",
3317 "progress_guard",
3318 "usage_limit_auto_continue",
3319 "tool_call_repair",
3320 "error_disclosure",
3321 "prompt_canary_guardrail",
3322 "guardrails",
3323 "user_hooks",
3324 "model_scout",
3325 "openrouter_workspace",
3326 "openrouter_server_tools",
3327 ]
3328 .into_iter()
3329 .collect::<BTreeSet<_>>();
3330 if cfg!(feature = "ui-capabilities") {
3331 ids.insert("openui");
3332 ids.insert("a2ui");
3333 }
3334 ids
3335 }
3336
3337 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3339 let mut ids = [
3340 "agent_instructions",
3341 "human_intent",
3342 "budgeting",
3343 "self_budget",
3344 "noop",
3345 "current_time",
3346 "session_file_system",
3347 "session_storage",
3348 "session",
3349 "stateless_todo_list",
3350 "bashkit_shell",
3351 "btw",
3352 "infinity_context",
3353 "compaction",
3354 "message_metadata",
3355 "openai_tool_search",
3356 "claude_tool_search",
3357 "tool_search",
3358 "auto_tool_search",
3359 "prompt_caching",
3360 "parallel_tool_calls",
3361 "skills",
3362 "system_commands",
3363 "tool_output_persistence",
3364 "tool_output_distillation",
3365 "loop_detection",
3366 "progress_guard",
3367 "tool_call_repair",
3368 "error_disclosure",
3369 "prompt_canary_guardrail",
3370 "guardrails",
3371 "user_hooks",
3372 ]
3373 .into_iter()
3374 .collect::<BTreeSet<_>>();
3375 if cfg!(feature = "web-fetch") {
3376 ids.insert("web_fetch");
3377 }
3378 ids
3379 }
3380
3381 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3383 let mut ids = expected_core_builtin_ids();
3384 ids.insert("agent_handoff");
3385 ids.insert("a2a_agent_delegation");
3386 ids
3387 }
3388
3389 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3390 registry.capabilities.keys().map(String::as_str).collect()
3391 }
3392
3393 #[test]
3403 fn test_capability_registry_with_builtins_dev() {
3404 let _lock = lock_env();
3406 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3407 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3408 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3409 assert!(registry.has("agent_handoff"));
3410 assert!(registry.has("a2a_agent_delegation"));
3411 }
3412
3413 #[test]
3414 fn test_capability_registry_with_builtins_prod() {
3415 let _lock = lock_env();
3417 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3418 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3419 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3420 assert!(!registry.has("docker_container"));
3422 assert!(!registry.has("agent_handoff"));
3423 assert!(!registry.has("a2a_agent_delegation"));
3424 }
3425
3426 #[test]
3427 fn test_capability_registry_runtime_builtins() {
3428 let _lock = lock_env();
3429 unsafe { std::env::remove_var("FEATURE_LUA") };
3430 let registry = CapabilityRegistry::runtime_builtins();
3431 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3432 assert!(registry.has("session_file_system"));
3433 #[cfg(feature = "web-fetch")]
3434 assert!(registry.has("web_fetch"));
3435 assert!(registry.has("bashkit_shell"));
3436
3437 for platform_only in [
3438 "platform_management",
3439 "model_scout",
3440 "openrouter_workspace",
3441 "openrouter_server_tools",
3442 "session_tasks",
3443 "session_schedule",
3444 "subagents",
3445 "background_execution",
3446 "session_sql_database",
3447 "knowledge_base",
3448 "knowledge_index",
3449 "sample_data",
3450 "data_knowledge",
3451 "fake_aws",
3452 "fake_crm",
3453 "fake_financial",
3454 "fake_warehouse",
3455 "test_math",
3456 "test_weather",
3457 "research",
3458 ] {
3459 assert!(
3460 !registry.has(platform_only),
3461 "`{platform_only}` should not be in the runtime default registry"
3462 );
3463 }
3464 }
3465
3466 #[test]
3467 fn test_agent_delegation_enabled_by_env_in_prod() {
3468 let _lock = lock_env();
3470 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3471 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3472 assert!(registry.has("agent_handoff"));
3473 assert!(registry.has("a2a_agent_delegation"));
3474 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3475 }
3476
3477 #[test]
3478 fn test_agent_delegation_disabled_by_env_in_dev() {
3479 let _lock = lock_env();
3481 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3482 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3483 assert!(!registry.has("agent_handoff"));
3484 assert!(!registry.has("a2a_agent_delegation"));
3485 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3486 }
3487
3488 #[test]
3489 fn test_capability_registry_get() {
3490 let registry = CapabilityRegistry::with_builtins();
3491
3492 let noop = registry.get("noop").unwrap();
3493 assert_eq!(noop.id(), "noop");
3494 assert_eq!(noop.name(), "No-Op");
3495 assert_eq!(noop.status(), CapabilityStatus::Available);
3496 }
3497
3498 #[test]
3506 fn builtin_capabilities_satisfy_registry_invariants() {
3507 let registry = CapabilityRegistry::with_builtins();
3508
3509 for cap in registry.list() {
3510 let id = cap.id();
3511 assert!(!id.is_empty(), "capability has an empty id");
3512 assert!(
3513 !cap.name().trim().is_empty(),
3514 "capability `{id}` has an empty name"
3515 );
3516
3517 assert!(
3520 registry.get(id).is_some(),
3521 "capability `{id}` does not resolve by its own id"
3522 );
3523
3524 for dep in cap.dependencies() {
3528 assert!(
3529 registry.get(dep).is_some(),
3530 "capability `{id}` depends on `{dep}`, which is not registered"
3531 );
3532 }
3533
3534 let mut seen = std::collections::HashSet::new();
3537 for tool in cap.tools() {
3538 let name = tool.name().to_string();
3539 assert!(
3540 !name.is_empty(),
3541 "capability `{id}` exposes a tool with an empty name"
3542 );
3543 assert!(
3544 seen.insert(name.clone()),
3545 "capability `{id}` exposes duplicate tool name `{name}`"
3546 );
3547 }
3548
3549 let mut def_seen = std::collections::HashSet::new();
3552 for def in cap.tool_definitions() {
3553 let name = def.name().to_string();
3554 assert!(
3555 !name.is_empty(),
3556 "capability `{id}` advertises a tool definition with an empty name"
3557 );
3558 assert!(
3559 def_seen.insert(name.clone()),
3560 "capability `{id}` advertises duplicate tool definition name `{name}`"
3561 );
3562 }
3563 }
3564 }
3565
3566 #[test]
3578 fn builtin_tools_have_narration_or_documented_generic_fallback() {
3579 use crate::tool_narration::{ToolNarrationContext, ToolNarrationPhase};
3580 use crate::tool_types::ToolCall;
3581
3582 const GENERIC_NARRATION_ALLOWLIST: &[(&str, &str)] = &[
3585 ("sample_data", "demo capability with fixture mounts"),
3587 (
3588 "data_knowledge",
3589 "demo knowledge scaffold; fixture data only",
3590 ),
3591 ("fake_aws", "demo/eval fixture tools"),
3592 ("fake_crm", "demo/eval fixture tools"),
3593 ("fake_financial", "demo/eval fixture tools"),
3594 ("fake_warehouse", "demo/eval fixture tools"),
3595 ("test_math", "test fixture capability"),
3596 ("test_weather", "test fixture capability"),
3597 (
3602 "platform_management",
3603 "operator admin surface; mutations narrate via narration_noun, reads use display names",
3604 ),
3605 (
3608 "model_scout",
3609 "operator model-routing tools; display-name presentation is adequate",
3610 ),
3611 (
3612 "openrouter_workspace",
3613 "operator OpenRouter inspection tools; display-name presentation is adequate",
3614 ),
3615 (
3618 "lua",
3619 "arbitrary sandboxed code execution; display-name presentation is adequate",
3620 ),
3621 ];
3622
3623 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3626 let ctx = ToolNarrationContext::default();
3627 let mut missing: Vec<String> = Vec::new();
3628
3629 for cap in registry.list() {
3630 let cap_id = cap.id().to_string();
3631 if GENERIC_NARRATION_ALLOWLIST
3632 .iter()
3633 .any(|(id, _)| *id == cap_id)
3634 {
3635 continue;
3636 }
3637
3638 for tool in cap.tools() {
3639 let def = tool.to_definition();
3640 if def.hints().narration_noun.is_some() {
3643 continue;
3644 }
3645
3646 let call = ToolCall {
3647 id: "call_narration_audit".to_string(),
3648 name: tool.name().to_string(),
3649 arguments: serde_json::json!({}),
3650 };
3651 if cap
3654 .narrate(Some(&def), &call, ToolNarrationPhase::Started, None, ctx)
3655 .is_none()
3656 {
3657 missing.push(format!("{cap_id}::{}", tool.name()));
3658 }
3659 }
3660 }
3661
3662 assert!(
3663 missing.is_empty(),
3664 "These built-in tools fall back to raw tool-call presentation. Implement \
3665 `Tool::narrate` (see specs/tool-narration.md), set a `narration_noun` hint, \
3666 or add a documented entry to GENERIC_NARRATION_ALLOWLIST: {missing:?}"
3667 );
3668 }
3669
3670 #[test]
3671 fn test_capability_registry_blueprint_with_capability() {
3672 struct BlueprintProviderCapability;
3673
3674 impl Capability for BlueprintProviderCapability {
3675 fn id(&self) -> &str {
3676 "blueprint_provider"
3677 }
3678 fn name(&self) -> &str {
3679 "Blueprint Provider"
3680 }
3681 fn description(&self) -> &str {
3682 "Capability that provides a blueprint for tests"
3683 }
3684 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3685 vec![AgentBlueprint {
3686 id: "test_blueprint",
3687 name: "Test Blueprint",
3688 description: "Blueprint for capability registry tests",
3689 model: BlueprintModel::Inherit,
3690 system_prompt: "Test prompt",
3691 tools: vec![],
3692 max_turns: None,
3693 config_schema: None,
3694 }]
3695 }
3696 }
3697
3698 let mut registry = CapabilityRegistry::new();
3699 registry.register(BlueprintProviderCapability);
3700
3701 let (capability_id, blueprint) = registry
3702 .blueprint_with_capability("test_blueprint")
3703 .expect("blueprint should resolve with capability id");
3704 assert_eq!(capability_id, "blueprint_provider");
3705 assert_eq!(blueprint.id, "test_blueprint");
3706 }
3707
3708 #[test]
3709 fn test_capability_registry_builder() {
3710 let registry = CapabilityRegistry::builder()
3711 .capability(NoopCapability)
3712 .capability(CurrentTimeCapability)
3713 .build();
3714
3715 assert!(registry.has("noop"));
3716 assert!(registry.has("current_time"));
3717 assert_eq!(registry.len(), 2);
3718 }
3719
3720 #[test]
3721 fn test_capability_status() {
3722 let registry = CapabilityRegistry::with_builtins();
3723
3724 let current_time = registry.get("current_time").unwrap();
3725 assert_eq!(current_time.status(), CapabilityStatus::Available);
3726
3727 let research = registry.get("research").unwrap();
3728 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3729 }
3730
3731 #[test]
3732 fn test_capability_icons_and_categories() {
3733 let registry = CapabilityRegistry::with_builtins();
3734
3735 let noop = registry.get("noop").unwrap();
3736 assert_eq!(noop.icon(), Some("circle-off"));
3737 assert_eq!(noop.category(), Some("Testing"));
3738
3739 let current_time = registry.get("current_time").unwrap();
3740 assert_eq!(current_time.icon(), Some("clock"));
3741 assert_eq!(current_time.category(), Some("Core"));
3742 }
3743
3744 #[test]
3745 fn test_system_prompt_preview_default_delegates_to_addition() {
3746 let registry = CapabilityRegistry::with_builtins();
3747
3748 let test_math = registry.get("test_math").unwrap();
3750 assert_eq!(
3751 test_math.system_prompt_preview().as_deref(),
3752 test_math.system_prompt_addition()
3753 );
3754
3755 let current_time = registry.get("current_time").unwrap();
3757 assert!(current_time.system_prompt_preview().is_none());
3758 assert!(current_time.system_prompt_addition().is_none());
3759 }
3760
3761 #[test]
3762 fn test_system_prompt_preview_dynamic_capability() {
3763 let registry = CapabilityRegistry::with_builtins();
3764 let cap = registry.get("agent_instructions").unwrap();
3765
3766 assert!(cap.system_prompt_addition().is_none());
3768 assert!(cap.system_prompt_preview().is_some());
3769 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3770 }
3771
3772 #[tokio::test]
3777 async fn test_apply_capabilities_empty() {
3778 let registry = CapabilityRegistry::with_builtins();
3779 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3780
3781 let applied =
3782 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3783
3784 assert_eq!(
3785 applied.runtime_agent.system_prompt,
3786 base_runtime_agent.system_prompt
3787 );
3788 assert!(applied.tool_registry.is_empty());
3789 assert!(applied.applied_ids.is_empty());
3790 }
3791
3792 #[tokio::test]
3793 async fn test_apply_capabilities_noop() {
3794 let registry = CapabilityRegistry::with_builtins();
3795 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3796
3797 let applied = apply_capabilities(
3798 base_runtime_agent.clone(),
3799 &["noop".to_string()],
3800 ®istry,
3801 &test_ctx(),
3802 )
3803 .await;
3804
3805 assert_eq!(
3807 applied.runtime_agent.system_prompt,
3808 base_runtime_agent.system_prompt
3809 );
3810 assert!(applied.tool_registry.is_empty());
3811 assert_eq!(applied.applied_ids, vec!["noop"]);
3812 }
3813
3814 #[tokio::test]
3815 async fn test_apply_capabilities_current_time() {
3816 let registry = CapabilityRegistry::with_builtins();
3817 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3818
3819 let applied = apply_capabilities(
3820 base_runtime_agent.clone(),
3821 &["current_time".to_string()],
3822 ®istry,
3823 &test_ctx(),
3824 )
3825 .await;
3826
3827 assert!(
3831 applied
3832 .runtime_agent
3833 .system_prompt
3834 .contains(FACTS_DYNAMIC_NOTE),
3835 "current_time should contribute the dynamic-facts note"
3836 );
3837 assert!(
3838 applied
3839 .runtime_agent
3840 .system_prompt
3841 .contains(&base_runtime_agent.system_prompt),
3842 "base prompt is preserved"
3843 );
3844 assert!(applied.tool_registry.has("get_current_time"));
3845 assert_eq!(applied.tool_registry.len(), 1);
3846 assert_eq!(applied.applied_ids, vec!["current_time"]);
3847 }
3848
3849 #[tokio::test]
3850 async fn test_apply_capabilities_skips_coming_soon() {
3851 let registry = CapabilityRegistry::with_builtins();
3852 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3853
3854 let applied = apply_capabilities(
3856 base_runtime_agent.clone(),
3857 &["research".to_string()],
3858 ®istry,
3859 &test_ctx(),
3860 )
3861 .await;
3862
3863 assert_eq!(
3865 applied.runtime_agent.system_prompt,
3866 base_runtime_agent.system_prompt
3867 );
3868 assert!(applied.applied_ids.is_empty()); }
3870
3871 #[tokio::test]
3872 async fn test_apply_capabilities_multiple() {
3873 let registry = CapabilityRegistry::with_builtins();
3874 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3875
3876 let applied = apply_capabilities(
3877 base_runtime_agent.clone(),
3878 &["noop".to_string(), "current_time".to_string()],
3879 ®istry,
3880 &test_ctx(),
3881 )
3882 .await;
3883
3884 assert!(applied.tool_registry.has("get_current_time"));
3885 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3886 }
3887
3888 #[tokio::test]
3889 async fn test_apply_capabilities_preserves_order() {
3890 let registry = CapabilityRegistry::with_builtins();
3891 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3892
3893 let applied = apply_capabilities(
3895 base_runtime_agent,
3896 &["current_time".to_string(), "noop".to_string()],
3897 ®istry,
3898 &test_ctx(),
3899 )
3900 .await;
3901
3902 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3903 }
3904
3905 #[tokio::test]
3906 async fn test_apply_capabilities_test_math() {
3907 let registry = CapabilityRegistry::with_builtins();
3908 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3909
3910 let applied = apply_capabilities(
3911 base_runtime_agent.clone(),
3912 &["test_math".to_string()],
3913 ®istry,
3914 &test_ctx(),
3915 )
3916 .await;
3917
3918 assert!(
3920 !applied
3921 .runtime_agent
3922 .system_prompt
3923 .contains("<capability id=\"test_math\">")
3924 );
3925 assert!(
3927 applied
3928 .runtime_agent
3929 .system_prompt
3930 .contains("You are a helpful assistant.")
3931 );
3932 assert!(applied.tool_registry.has("add"));
3933 assert!(applied.tool_registry.has("subtract"));
3934 assert!(applied.tool_registry.has("multiply"));
3935 assert!(applied.tool_registry.has("divide"));
3936 assert_eq!(applied.tool_registry.len(), 4);
3937 }
3938
3939 #[tokio::test]
3940 async fn test_apply_capabilities_test_weather() {
3941 let registry = CapabilityRegistry::with_builtins();
3942 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3943
3944 let applied = apply_capabilities(
3945 base_runtime_agent.clone(),
3946 &["test_weather".to_string()],
3947 ®istry,
3948 &test_ctx(),
3949 )
3950 .await;
3951
3952 assert!(
3954 !applied
3955 .runtime_agent
3956 .system_prompt
3957 .contains("<capability id=\"test_weather\">")
3958 );
3959 assert!(applied.tool_registry.has("get_weather"));
3960 assert!(applied.tool_registry.has("get_forecast"));
3961 assert_eq!(applied.tool_registry.len(), 2);
3962 }
3963
3964 #[tokio::test]
3965 async fn test_apply_capabilities_test_math_and_test_weather() {
3966 let registry = CapabilityRegistry::with_builtins();
3967 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3968
3969 let applied = apply_capabilities(
3970 base_runtime_agent.clone(),
3971 &["test_math".to_string(), "test_weather".to_string()],
3972 ®istry,
3973 &test_ctx(),
3974 )
3975 .await;
3976
3977 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3980 assert!(applied.tool_registry.has("get_weather"));
3981 }
3982
3983 #[tokio::test]
3984 async fn test_apply_capabilities_stateless_todo_list() {
3985 let registry = CapabilityRegistry::with_builtins();
3986 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3987
3988 let applied = apply_capabilities(
3989 base_runtime_agent.clone(),
3990 &["stateless_todo_list".to_string()],
3991 ®istry,
3992 &test_ctx(),
3993 )
3994 .await;
3995
3996 assert!(
3998 applied
3999 .runtime_agent
4000 .system_prompt
4001 .contains("Task Management")
4002 );
4003 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
4004 assert!(applied.tool_registry.has("write_todos"));
4005 assert_eq!(applied.tool_registry.len(), 1);
4006 }
4007
4008 #[tokio::test]
4009 async fn test_apply_capabilities_web_fetch() {
4010 let registry = CapabilityRegistry::with_builtins();
4011 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
4012
4013 let applied = apply_capabilities(
4014 base_runtime_agent.clone(),
4015 &["web_fetch".to_string()],
4016 ®istry,
4017 &test_ctx(),
4018 )
4019 .await;
4020
4021 assert!(
4023 applied
4024 .runtime_agent
4025 .system_prompt
4026 .contains(&base_runtime_agent.system_prompt)
4027 );
4028 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
4029 assert!(applied.tool_registry.has("web_fetch"));
4030 assert_eq!(applied.tool_registry.len(), 1);
4031 }
4032
4033 #[tokio::test]
4038 async fn test_xml_tags_wrap_capability_prompts() {
4039 let registry = CapabilityRegistry::with_builtins();
4040 let collected =
4041 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
4042 .await;
4043
4044 assert_eq!(collected.system_prompt_parts.len(), 1);
4045 let part = &collected.system_prompt_parts[0];
4046 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
4047 assert!(part.ends_with("</capability>"));
4048 assert!(part.contains("Task Management"));
4049 }
4050
4051 #[tokio::test]
4052 async fn test_xml_tags_multiple_capabilities() {
4053 let registry = CapabilityRegistry::with_builtins();
4054 let collected = collect_capabilities(
4055 &[
4056 "stateless_todo_list".to_string(),
4057 "session_schedule".to_string(),
4058 ],
4059 ®istry,
4060 &test_ctx(),
4061 )
4062 .await;
4063
4064 assert_eq!(collected.system_prompt_parts.len(), 2);
4065 assert!(
4066 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
4067 );
4068 assert!(
4069 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
4070 );
4071
4072 let prefix = collected.system_prompt_prefix().unwrap();
4073 assert!(prefix.contains("</capability>\n\n<capability"));
4075 }
4076
4077 #[tokio::test]
4078 async fn test_xml_tags_system_prompt_wrapping() {
4079 let registry = CapabilityRegistry::with_builtins();
4080 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4081
4082 let applied = apply_capabilities(
4083 base,
4084 &["stateless_todo_list".to_string()],
4085 ®istry,
4086 &test_ctx(),
4087 )
4088 .await;
4089
4090 let prompt = &applied.runtime_agent.system_prompt;
4091 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4092 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
4094 assert!(prompt.contains("</capability>"));
4095 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4097 }
4098
4099 #[tokio::test]
4100 async fn test_no_xml_wrapping_without_capabilities() {
4101 let registry = CapabilityRegistry::with_builtins();
4102 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4103
4104 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
4105
4106 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4108 assert!(
4109 !applied
4110 .runtime_agent
4111 .system_prompt
4112 .contains("<system-prompt>")
4113 );
4114 }
4115
4116 #[tokio::test]
4117 async fn test_no_xml_wrapping_for_noop_capability() {
4118 let registry = CapabilityRegistry::with_builtins();
4119 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4120
4121 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
4123
4124 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4125 assert!(
4126 !applied
4127 .runtime_agent
4128 .system_prompt
4129 .contains("<system-prompt>")
4130 );
4131 }
4132
4133 #[tokio::test]
4138 async fn test_collect_capabilities_includes_mounts() {
4139 let registry = CapabilityRegistry::with_builtins();
4140
4141 let collected =
4142 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4143
4144 assert!(!collected.mounts.is_empty());
4145 assert_eq!(collected.mounts.len(), 1);
4146 assert_eq!(collected.mounts[0].path, "/samples");
4147 assert!(collected.mounts[0].is_readonly());
4148 }
4149
4150 #[tokio::test]
4151 async fn test_collect_capabilities_empty_mounts_by_default() {
4152 let registry = CapabilityRegistry::with_builtins();
4153
4154 let collected =
4156 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4157
4158 assert!(collected.mounts.is_empty());
4159 }
4160
4161 #[tokio::test]
4162 async fn test_dynamic_facts_add_note_without_static_block() {
4163 let registry = CapabilityRegistry::with_builtins();
4167 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4168 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4169 let prompt = collected.system_prompt_parts.join("\n");
4170 assert!(
4171 prompt.contains(FACTS_DYNAMIC_NOTE),
4172 "dynamic-facts note should be in the cached prompt"
4173 );
4174 assert!(
4175 !prompt.contains("<facts>\n"),
4176 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
4177 );
4178 }
4179
4180 #[tokio::test]
4181 async fn test_static_facts_fold_into_prompt() {
4182 struct StaticFactCap;
4183 impl Capability for StaticFactCap {
4184 fn id(&self) -> &str {
4185 "test_static_fact"
4186 }
4187 fn name(&self) -> &str {
4188 "Static Fact"
4189 }
4190 fn description(&self) -> &str {
4191 "test"
4192 }
4193 fn status(&self) -> CapabilityStatus {
4194 CapabilityStatus::Available
4195 }
4196 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
4197 vec![Fact::stat("workspace_root", "/workspace")]
4198 }
4199 }
4200 let mut registry = CapabilityRegistry::new();
4201 registry.register(StaticFactCap);
4202 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
4203 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4204 let prompt = collected.system_prompt_parts.join("\n");
4205 assert!(
4206 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
4207 "static fact should fold into the cached prompt; got: {prompt}"
4208 );
4209 assert!(
4210 !prompt.contains(FACTS_DYNAMIC_NOTE),
4211 "no dynamic note when only static facts exist"
4212 );
4213 }
4214
4215 #[test]
4216 fn test_collect_dynamic_facts_returns_current_time() {
4217 let registry = CapabilityRegistry::with_builtins();
4218 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4219 let facts = collect_dynamic_facts(
4220 &configs,
4221 ®istry,
4222 None,
4223 &FactsContext::new(SessionId::new()),
4224 );
4225 assert_eq!(facts.len(), 1);
4226 assert_eq!(facts[0].key, "current_time");
4227 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4228 }
4229
4230 #[tokio::test]
4231 async fn test_collect_capabilities_combines_mounts() {
4232 let registry = CapabilityRegistry::with_builtins();
4233
4234 let collected = collect_capabilities(
4237 &["sample_data".to_string(), "current_time".to_string()],
4238 ®istry,
4239 &test_ctx(),
4240 )
4241 .await;
4242
4243 assert_eq!(collected.mounts.len(), 1);
4244 assert!(
4246 collected
4247 .applied_ids
4248 .iter()
4249 .any(|id| id == "session_file_system")
4250 );
4251 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4252 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4253 }
4254
4255 #[test]
4256 fn test_sample_data_capability() {
4257 let registry = CapabilityRegistry::with_builtins();
4258 let cap = registry.get("sample_data").unwrap();
4259
4260 assert_eq!(cap.id(), "sample_data");
4261 assert_eq!(cap.name(), "Sample Data");
4262 assert_eq!(cap.status(), CapabilityStatus::Available);
4263
4264 assert!(cap.system_prompt_addition().is_some());
4266 assert!(cap.tools().is_empty());
4267
4268 assert!(!cap.mounts().is_empty());
4270 }
4271
4272 #[test]
4277 fn test_resolve_dependencies_empty() {
4278 let registry = CapabilityRegistry::with_builtins();
4279
4280 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4281
4282 assert!(resolved.resolved_ids.is_empty());
4283 assert!(resolved.added_as_dependencies.is_empty());
4284 assert!(resolved.user_selected.is_empty());
4285 }
4286
4287 #[test]
4288 fn test_resolve_dependencies_no_deps() {
4289 let registry = CapabilityRegistry::with_builtins();
4290
4291 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4293
4294 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4295 assert!(resolved.added_as_dependencies.is_empty());
4296 }
4297
4298 #[test]
4299 fn test_resolve_dependencies_with_deps() {
4300 let registry = CapabilityRegistry::with_builtins();
4301
4302 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4304
4305 assert_eq!(resolved.resolved_ids.len(), 2);
4307 let fs_pos = resolved
4308 .resolved_ids
4309 .iter()
4310 .position(|id| id == "session_file_system")
4311 .unwrap();
4312 let sd_pos = resolved
4313 .resolved_ids
4314 .iter()
4315 .position(|id| id == "sample_data")
4316 .unwrap();
4317 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4318
4319 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4321 }
4322
4323 #[test]
4324 fn test_resolve_dependencies_already_selected() {
4325 let registry = CapabilityRegistry::with_builtins();
4326
4327 let resolved = resolve_dependencies(
4329 &["session_file_system".to_string(), "sample_data".to_string()],
4330 ®istry,
4331 )
4332 .unwrap();
4333
4334 assert_eq!(resolved.resolved_ids.len(), 2);
4335 assert!(resolved.added_as_dependencies.is_empty());
4337 }
4338
4339 #[test]
4340 fn test_resolve_dependencies_preserves_order() {
4341 let registry = CapabilityRegistry::with_builtins();
4342
4343 let resolved =
4345 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4346 .unwrap();
4347
4348 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4349 }
4350
4351 #[test]
4352 fn test_resolve_dependencies_unknown_capability() {
4353 let registry = CapabilityRegistry::with_builtins();
4354
4355 let resolved =
4357 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4358
4359 assert!(resolved.resolved_ids.is_empty());
4360 }
4361
4362 #[test]
4363 fn test_get_dependencies() {
4364 let registry = CapabilityRegistry::with_builtins();
4365
4366 let deps = get_dependencies("sample_data", ®istry);
4368 assert_eq!(deps, vec!["session_file_system"]);
4369
4370 let deps = get_dependencies("current_time", ®istry);
4372 assert!(deps.is_empty());
4373
4374 let deps = get_dependencies("unknown", ®istry);
4376 assert!(deps.is_empty());
4377 }
4378
4379 #[test]
4380 fn test_sample_data_has_dependency() {
4381 let registry = CapabilityRegistry::with_builtins();
4382 let cap = registry.get("sample_data").unwrap();
4383
4384 let deps = cap.dependencies();
4385 assert_eq!(deps.len(), 1);
4386 assert_eq!(deps[0], "session_file_system");
4387 }
4388
4389 #[test]
4390 fn test_noop_has_no_dependencies() {
4391 let registry = CapabilityRegistry::with_builtins();
4392 let cap = registry.get("noop").unwrap();
4393
4394 assert!(cap.dependencies().is_empty());
4395 }
4396
4397 #[test]
4401 fn test_circular_dependency_error() {
4402 struct CapA;
4404 struct CapB;
4405
4406 impl Capability for CapA {
4407 fn id(&self) -> &str {
4408 "test_cap_a"
4409 }
4410 fn name(&self) -> &str {
4411 "Test A"
4412 }
4413 fn description(&self) -> &str {
4414 "Test capability A"
4415 }
4416 fn dependencies(&self) -> Vec<&'static str> {
4417 vec!["test_cap_b"]
4418 }
4419 }
4420
4421 impl Capability for CapB {
4422 fn id(&self) -> &str {
4423 "test_cap_b"
4424 }
4425 fn name(&self) -> &str {
4426 "Test B"
4427 }
4428 fn description(&self) -> &str {
4429 "Test capability B"
4430 }
4431 fn dependencies(&self) -> Vec<&'static str> {
4432 vec!["test_cap_a"]
4433 }
4434 }
4435
4436 let mut registry = CapabilityRegistry::new();
4437 registry.register(CapA);
4438 registry.register(CapB);
4439
4440 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4441
4442 assert!(result.is_err());
4443 match result.unwrap_err() {
4444 DependencyError::CircularDependency { capability_id, .. } => {
4445 assert_eq!(capability_id, "test_cap_a");
4446 }
4447 _ => panic!("Expected CircularDependency error"),
4448 }
4449 }
4450
4451 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4456
4457 struct FilterTestCapability {
4459 priority: i32,
4460 }
4461
4462 impl Capability for FilterTestCapability {
4463 fn id(&self) -> &str {
4464 "filter_test"
4465 }
4466 fn name(&self) -> &str {
4467 "Filter Test"
4468 }
4469 fn description(&self) -> &str {
4470 "Test capability with message filter"
4471 }
4472 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4473 Some(Arc::new(FilterTestProvider {
4474 priority: self.priority,
4475 }))
4476 }
4477 }
4478
4479 struct FilterTestProvider {
4480 priority: i32,
4481 }
4482
4483 impl MessageFilterProvider for FilterTestProvider {
4484 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4485 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4487 query
4488 .filters
4489 .push(MessageFilter::Search(search.to_string()));
4490 }
4491 }
4492
4493 fn priority(&self) -> i32 {
4494 self.priority
4495 }
4496 }
4497
4498 #[tokio::test]
4499 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4500 let registry = CapabilityRegistry::with_builtins();
4501 let configs = vec![AgentCapabilityConfig {
4502 capability_ref: CapabilityId::new("current_time"),
4503 config: serde_json::json!({}),
4504 }];
4505
4506 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4507
4508 assert!(collected.message_filter_providers.is_empty());
4509 assert!(!collected.has_message_filters());
4510 }
4511
4512 #[tokio::test]
4513 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4514 let mut registry = CapabilityRegistry::new();
4515 registry.register(FilterTestCapability { priority: 0 });
4516
4517 let configs = vec![AgentCapabilityConfig {
4518 capability_ref: CapabilityId::new("filter_test"),
4519 config: serde_json::json!({ "search": "hello" }),
4520 }];
4521
4522 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4523
4524 assert_eq!(collected.message_filter_providers.len(), 1);
4525 assert!(collected.has_message_filters());
4526 }
4527
4528 #[tokio::test]
4529 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4530 struct HighPriorityCapability;
4532 struct LowPriorityCapability;
4533
4534 impl Capability for HighPriorityCapability {
4535 fn id(&self) -> &str {
4536 "high_priority"
4537 }
4538 fn name(&self) -> &str {
4539 "High Priority"
4540 }
4541 fn description(&self) -> &str {
4542 "Test"
4543 }
4544 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4545 Some(Arc::new(FilterTestProvider { priority: 10 }))
4546 }
4547 }
4548
4549 impl Capability for LowPriorityCapability {
4550 fn id(&self) -> &str {
4551 "low_priority"
4552 }
4553 fn name(&self) -> &str {
4554 "Low Priority"
4555 }
4556 fn description(&self) -> &str {
4557 "Test"
4558 }
4559 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4560 Some(Arc::new(FilterTestProvider { priority: -5 }))
4561 }
4562 }
4563
4564 let mut registry = CapabilityRegistry::new();
4565 registry.register(HighPriorityCapability);
4566 registry.register(LowPriorityCapability);
4567
4568 let configs = vec![
4570 AgentCapabilityConfig {
4571 capability_ref: CapabilityId::new("high_priority"),
4572 config: serde_json::json!({}),
4573 },
4574 AgentCapabilityConfig {
4575 capability_ref: CapabilityId::new("low_priority"),
4576 config: serde_json::json!({}),
4577 },
4578 ];
4579
4580 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4581
4582 assert_eq!(collected.message_filter_providers.len(), 2);
4584 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4585 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4586 }
4587
4588 #[tokio::test]
4589 async fn test_collected_capabilities_apply_message_filters() {
4590 let mut registry = CapabilityRegistry::new();
4591 registry.register(FilterTestCapability { priority: 0 });
4592
4593 let configs = vec![AgentCapabilityConfig {
4594 capability_ref: CapabilityId::new("filter_test"),
4595 config: serde_json::json!({ "search": "test_query" }),
4596 }];
4597
4598 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4599
4600 let session_id: SessionId = Uuid::now_v7().into();
4602 let mut query = MessageQuery::new(session_id);
4603
4604 collected.apply_message_filters(&mut query);
4605
4606 assert_eq!(query.filters.len(), 1);
4608 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4609 }
4610
4611 #[tokio::test]
4612 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4613 struct SearchCapability {
4614 id: &'static str,
4615 search_term: &'static str,
4616 priority: i32,
4617 }
4618
4619 struct SearchProvider {
4620 search_term: &'static str,
4621 priority: i32,
4622 }
4623
4624 impl MessageFilterProvider for SearchProvider {
4625 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4626 query
4627 .filters
4628 .push(MessageFilter::Search(self.search_term.to_string()));
4629 }
4630
4631 fn priority(&self) -> i32 {
4632 self.priority
4633 }
4634 }
4635
4636 impl Capability for SearchCapability {
4637 fn id(&self) -> &str {
4638 self.id
4639 }
4640 fn name(&self) -> &str {
4641 "Search"
4642 }
4643 fn description(&self) -> &str {
4644 "Test"
4645 }
4646 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4647 Some(Arc::new(SearchProvider {
4648 search_term: self.search_term,
4649 priority: self.priority,
4650 }))
4651 }
4652 }
4653
4654 let mut registry = CapabilityRegistry::new();
4655 registry.register(SearchCapability {
4656 id: "cap_a",
4657 search_term: "alpha",
4658 priority: 5,
4659 });
4660 registry.register(SearchCapability {
4661 id: "cap_b",
4662 search_term: "beta",
4663 priority: 1,
4664 });
4665 registry.register(SearchCapability {
4666 id: "cap_c",
4667 search_term: "gamma",
4668 priority: 10,
4669 });
4670
4671 let configs = vec![
4672 AgentCapabilityConfig {
4673 capability_ref: CapabilityId::new("cap_a"),
4674 config: serde_json::json!({}),
4675 },
4676 AgentCapabilityConfig {
4677 capability_ref: CapabilityId::new("cap_b"),
4678 config: serde_json::json!({}),
4679 },
4680 AgentCapabilityConfig {
4681 capability_ref: CapabilityId::new("cap_c"),
4682 config: serde_json::json!({}),
4683 },
4684 ];
4685
4686 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4687
4688 let session_id: SessionId = Uuid::now_v7().into();
4689 let mut query = MessageQuery::new(session_id);
4690
4691 collected.apply_message_filters(&mut query);
4692
4693 assert_eq!(query.filters.len(), 3);
4695 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4696 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4697 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4698 }
4699
4700 #[test]
4701 fn test_capability_without_message_filter_returns_none() {
4702 let registry = CapabilityRegistry::with_builtins();
4703
4704 let noop = registry.get("noop").unwrap();
4705 assert!(noop.message_filter_provider().is_none());
4706
4707 let current_time = registry.get("current_time").unwrap();
4708 assert!(current_time.message_filter_provider().is_none());
4709 }
4710
4711 #[tokio::test]
4712 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4713 let mut registry = CapabilityRegistry::new();
4714 registry.register(FilterTestCapability { priority: 0 });
4715
4716 let test_config = serde_json::json!({
4717 "search": "custom_search",
4718 "extra_field": 42
4719 });
4720
4721 let configs = vec![AgentCapabilityConfig {
4722 capability_ref: CapabilityId::new("filter_test"),
4723 config: test_config.clone(),
4724 }];
4725
4726 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4727
4728 assert_eq!(collected.message_filter_providers.len(), 1);
4730 let (_, stored_config) = &collected.message_filter_providers[0];
4731 assert_eq!(*stored_config, test_config);
4732 }
4733
4734 #[test]
4739 fn test_collect_message_filters_only_collects_filters() {
4740 let mut registry = CapabilityRegistry::new();
4741 registry.register(FilterTestCapability { priority: 0 });
4742
4743 let configs = vec![AgentCapabilityConfig {
4744 capability_ref: CapabilityId::new("filter_test"),
4745 config: serde_json::json!({ "search": "test_query" }),
4746 }];
4747
4748 let collected = collect_message_filters_only(&configs, ®istry);
4749
4750 let session_id: SessionId = Uuid::now_v7().into();
4751 let mut query = MessageQuery::new(session_id);
4752 collected.apply_message_filters(&mut query);
4753
4754 assert_eq!(query.filters.len(), 1);
4755 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4756 }
4757
4758 #[test]
4759 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4760 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4761
4762 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4764 assert_eq!(with["compaction_active"], serde_json::json!(true));
4765 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4766
4767 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4768 assert!(without.get("compaction_active").is_none());
4769
4770 let other = message_filter_config_for("other", &base, true);
4772 assert!(other.get("compaction_active").is_none());
4773
4774 let null_base = message_filter_config_for(
4776 INFINITY_CONTEXT_CAPABILITY_ID,
4777 &serde_json::Value::Null,
4778 true,
4779 );
4780 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4781 }
4782
4783 #[test]
4784 fn test_infinity_context_defers_to_compaction_end_to_end() {
4785 use crate::message::Message;
4786
4787 let mut registry = CapabilityRegistry::new();
4788 registry.register(InfinityContextCapability);
4789 registry.register(CompactionCapability);
4790
4791 let tight = serde_json::json!({
4792 "context_budget_tokens": 1,
4793 "min_recent_messages": 1
4794 });
4795
4796 let solo = vec![AgentCapabilityConfig {
4798 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4799 config: tight.clone(),
4800 }];
4801 let mut messages = vec![
4802 Message::user("task"),
4803 Message::assistant("old ".repeat(400)),
4804 Message::user("recent"),
4805 ];
4806 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4807 assert!(
4808 messages
4809 .iter()
4810 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4811 "infinity context alone should trim and notice"
4812 );
4813
4814 let both = vec![
4816 AgentCapabilityConfig {
4817 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4818 config: tight,
4819 },
4820 AgentCapabilityConfig {
4821 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4822 config: serde_json::json!({}),
4823 },
4824 ];
4825 let mut messages = vec![
4826 Message::user("task"),
4827 Message::assistant("old ".repeat(400)),
4828 Message::user("recent"),
4829 ];
4830 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4831 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4832 assert!(
4833 messages
4834 .iter()
4835 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4836 "no hidden-history notice when compaction is the active reducer"
4837 );
4838 }
4839
4840 #[test]
4841 fn test_compaction_is_enabled_detects_compaction() {
4842 let mut registry = CapabilityRegistry::new();
4843 registry.register(CompactionCapability);
4844
4845 let with_compaction = vec![AgentCapabilityConfig {
4846 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4847 config: serde_json::json!({}),
4848 }];
4849 assert!(compaction_is_enabled(&with_compaction, ®istry));
4850
4851 let without = vec![AgentCapabilityConfig {
4852 capability_ref: CapabilityId::new("current_time"),
4853 config: serde_json::json!({}),
4854 }];
4855 assert!(!compaction_is_enabled(&without, ®istry));
4856 }
4857
4858 #[test]
4859 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4860 let registry = CapabilityRegistry::new();
4861
4862 let configs = vec![AgentCapabilityConfig {
4863 capability_ref: CapabilityId::new("nonexistent"),
4864 config: serde_json::json!({}),
4865 }];
4866
4867 let collected = collect_message_filters_only(&configs, ®istry);
4868 assert!(collected.message_filter_providers.is_empty());
4869 }
4870
4871 #[test]
4872 fn test_collect_message_filters_only_preserves_priority_order() {
4873 struct PriorityFilterCap {
4874 id: &'static str,
4875 search_term: &'static str,
4876 priority: i32,
4877 }
4878
4879 struct PriorityFilterProvider {
4880 search_term: &'static str,
4881 priority: i32,
4882 }
4883
4884 impl Capability for PriorityFilterCap {
4885 fn id(&self) -> &str {
4886 self.id
4887 }
4888 fn name(&self) -> &str {
4889 self.id
4890 }
4891 fn description(&self) -> &str {
4892 "priority test"
4893 }
4894 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4895 Some(Arc::new(PriorityFilterProvider {
4896 search_term: self.search_term,
4897 priority: self.priority,
4898 }))
4899 }
4900 }
4901
4902 impl MessageFilterProvider for PriorityFilterProvider {
4903 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4904 query
4905 .filters
4906 .push(MessageFilter::Search(self.search_term.to_string()));
4907 }
4908 fn priority(&self) -> i32 {
4909 self.priority
4910 }
4911 }
4912
4913 let mut registry = CapabilityRegistry::new();
4914 registry.register(PriorityFilterCap {
4915 id: "gamma",
4916 search_term: "gamma",
4917 priority: 10,
4918 });
4919 registry.register(PriorityFilterCap {
4920 id: "alpha",
4921 search_term: "alpha",
4922 priority: 5,
4923 });
4924 registry.register(PriorityFilterCap {
4925 id: "beta",
4926 search_term: "beta",
4927 priority: 1,
4928 });
4929
4930 let configs = vec![
4931 AgentCapabilityConfig {
4932 capability_ref: CapabilityId::new("gamma"),
4933 config: serde_json::json!({}),
4934 },
4935 AgentCapabilityConfig {
4936 capability_ref: CapabilityId::new("alpha"),
4937 config: serde_json::json!({}),
4938 },
4939 AgentCapabilityConfig {
4940 capability_ref: CapabilityId::new("beta"),
4941 config: serde_json::json!({}),
4942 },
4943 ];
4944
4945 let collected = collect_message_filters_only(&configs, ®istry);
4946
4947 let session_id: SessionId = Uuid::now_v7().into();
4948 let mut query = MessageQuery::new(session_id);
4949 collected.apply_message_filters(&mut query);
4950
4951 assert_eq!(query.filters.len(), 3);
4953 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4954 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4955 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4956 }
4957
4958 #[test]
4959 fn test_collect_message_filters_only_post_load_invoked() {
4960 use crate::message::Message;
4961
4962 struct PostLoadCap;
4963 struct PostLoadProvider;
4964
4965 impl Capability for PostLoadCap {
4966 fn id(&self) -> &str {
4967 "post_load_test"
4968 }
4969 fn name(&self) -> &str {
4970 "PostLoad Test"
4971 }
4972 fn description(&self) -> &str {
4973 "test"
4974 }
4975 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4976 Some(Arc::new(PostLoadProvider))
4977 }
4978 }
4979
4980 impl MessageFilterProvider for PostLoadProvider {
4981 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4982 fn priority(&self) -> i32 {
4983 0
4984 }
4985 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4986 messages.reverse();
4988 }
4989 }
4990
4991 let mut registry = CapabilityRegistry::new();
4992 registry.register(PostLoadCap);
4993
4994 let configs = vec![AgentCapabilityConfig {
4995 capability_ref: CapabilityId::new("post_load_test"),
4996 config: serde_json::json!({}),
4997 }];
4998
4999 let collected = collect_message_filters_only(&configs, ®istry);
5000
5001 let mut messages = vec![Message::user("first"), Message::user("second")];
5002 collected.apply_post_load_filters(&mut messages);
5003
5004 assert_eq!(messages[0].text(), Some("second"));
5006 assert_eq!(messages[1].text(), Some("first"));
5007 }
5008
5009 #[test]
5010 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
5011 use crate::tool_types::ToolCall;
5012
5013 fn tool_heavy_messages() -> Vec<Message> {
5014 let mut messages = vec![Message::user("inspect files repeatedly")];
5015 for index in 0..9 {
5016 let call_id = format!("call_{index}");
5017 messages.push(Message::assistant_with_tools(
5018 "",
5019 vec![ToolCall {
5020 id: call_id.clone(),
5021 name: "read_file".to_string(),
5022 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
5023 }],
5024 ));
5025 messages.push(Message::tool_result(
5026 call_id,
5027 Some(serde_json::json!({
5028 "path": "/workspace/src/lib.rs",
5029 "content": format!("{}{}", "large file line\n".repeat(1000), index),
5030 "total_lines": 1000,
5031 "lines_shown": {"start": 1, "end": 1000},
5032 "truncated": false
5033 })),
5034 None,
5035 ));
5036 }
5037 messages
5038 }
5039
5040 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
5041 messages[2]
5042 .tool_result_content()
5043 .and_then(|result| result.result.as_ref())
5044 .and_then(|result| result.get("masked"))
5045 .and_then(|masked| masked.as_bool())
5046 .unwrap_or(false)
5047 }
5048
5049 let mut registry = CapabilityRegistry::new();
5050 registry.register(CompactionCapability);
5051 let context = ModelViewContext {
5052 session_id: SessionId::new(),
5053 prior_usage: None,
5054 };
5055
5056 let no_compaction = collect_model_view_providers(&[], ®istry, None);
5057 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
5058 assert!(!first_tool_result_is_masked(&unmasked));
5059
5060 let compaction = collect_model_view_providers(
5061 &[AgentCapabilityConfig {
5062 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
5063 config: serde_json::json!({}),
5064 }],
5065 ®istry,
5066 None,
5067 );
5068 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
5069 assert!(first_tool_result_is_masked(&masked));
5070 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
5071 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
5072 }
5073
5074 struct DelegatingFilterCap {
5077 id: &'static str,
5078 inner: std::sync::Arc<InnerFilterCap>,
5079 }
5080 struct InnerFilterCap;
5081
5082 impl Capability for InnerFilterCap {
5083 fn id(&self) -> &str {
5084 "inner_filter"
5085 }
5086 fn name(&self) -> &str {
5087 "Inner Filter"
5088 }
5089 fn description(&self) -> &str {
5090 "inner"
5091 }
5092 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5093 Some(std::sync::Arc::new(SentinelFilter))
5094 }
5095 }
5096 struct SentinelFilter;
5097 impl MessageFilterProvider for SentinelFilter {
5098 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
5099 }
5100 impl Capability for DelegatingFilterCap {
5101 fn id(&self) -> &str {
5102 self.id
5103 }
5104 fn name(&self) -> &str {
5105 "Delegating Filter"
5106 }
5107 fn description(&self) -> &str {
5108 "delegating"
5109 }
5110 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5111 None }
5113 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5114 Some(&*self.inner)
5115 }
5116 }
5117
5118 #[test]
5119 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
5120 let inner = std::sync::Arc::new(InnerFilterCap);
5121 let outer = DelegatingFilterCap {
5122 id: "delegating_filter",
5123 inner: inner.clone(),
5124 };
5125
5126 let mut registry = CapabilityRegistry::new();
5127 registry.register(outer);
5128
5129 let configs = vec![AgentCapabilityConfig {
5130 capability_ref: CapabilityId::new("delegating_filter"),
5131 config: serde_json::json!({}),
5132 }];
5133
5134 let collected = collect_message_filters_only(&configs, ®istry);
5137 assert_eq!(
5138 collected.message_filter_providers.len(),
5139 1,
5140 "provider from resolved inner capability must be collected"
5141 );
5142 }
5143
5144 struct DelegatingMvpCap {
5145 id: &'static str,
5146 inner: std::sync::Arc<InnerMvpCap>,
5147 }
5148 struct InnerMvpCap;
5149
5150 impl Capability for InnerMvpCap {
5151 fn id(&self) -> &str {
5152 "inner_mvp"
5153 }
5154 fn name(&self) -> &str {
5155 "Inner MVP"
5156 }
5157 fn description(&self) -> &str {
5158 "inner"
5159 }
5160 fn model_view_provider(
5161 &self,
5162 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5163 struct NoopMvp;
5165 impl crate::capabilities::ModelViewProvider for NoopMvp {
5166 fn apply_model_view(
5167 &self,
5168 messages: Vec<Message>,
5169 _config: &serde_json::Value,
5170 _context: &ModelViewContext<'_>,
5171 ) -> Vec<Message> {
5172 messages
5173 }
5174 }
5175 Some(std::sync::Arc::new(NoopMvp))
5176 }
5177 }
5178 impl Capability for DelegatingMvpCap {
5179 fn id(&self) -> &str {
5180 self.id
5181 }
5182 fn name(&self) -> &str {
5183 "Delegating MVP"
5184 }
5185 fn description(&self) -> &str {
5186 "delegating"
5187 }
5188 fn model_view_provider(
5189 &self,
5190 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5191 None }
5193 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5194 Some(&*self.inner)
5195 }
5196 }
5197
5198 #[test]
5199 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
5200 let inner = std::sync::Arc::new(InnerMvpCap);
5201 let outer = DelegatingMvpCap {
5202 id: "delegating_mvp",
5203 inner: inner.clone(),
5204 };
5205
5206 let mut registry = CapabilityRegistry::new();
5207 registry.register(outer);
5208
5209 let configs = vec![AgentCapabilityConfig {
5210 capability_ref: CapabilityId::new("delegating_mvp"),
5211 config: serde_json::json!({}),
5212 }];
5213
5214 let collected = collect_model_view_providers(&configs, ®istry, None);
5217 assert_eq!(
5218 collected.model_view_providers.len(),
5219 1,
5220 "provider from resolved inner capability must be collected"
5221 );
5222 }
5223
5224 #[tokio::test]
5234 async fn test_bashkit_shell_capability_produces_bash_tool() {
5235 let registry = CapabilityRegistry::with_builtins();
5236 let collected =
5237 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5238
5239 let tool_names: Vec<&str> = collected
5240 .tool_definitions
5241 .iter()
5242 .map(|t| t.name())
5243 .collect();
5244 assert!(
5245 tool_names.contains(&"bash"),
5246 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5247 tool_names
5248 );
5249 assert!(
5250 !collected.tools.is_empty(),
5251 "bashkit_shell must provide tool implementations"
5252 );
5253 }
5254
5255 #[tokio::test]
5256 async fn test_generic_harness_capability_set_produces_bash_tool() {
5257 let generic_harness_caps = vec![
5260 "session_file_system".to_string(),
5261 "bashkit_shell".to_string(),
5262 "web_fetch".to_string(),
5263 "session_storage".to_string(),
5264 "session".to_string(),
5265 "agent_instructions".to_string(),
5266 "skills".to_string(),
5267 "infinity_context".to_string(),
5268 "auto_tool_search".to_string(),
5269 ];
5270
5271 let registry = CapabilityRegistry::with_builtins();
5272 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5273
5274 let tool_names: Vec<&str> = collected
5275 .tool_definitions
5276 .iter()
5277 .map(|t| t.name())
5278 .collect();
5279 assert!(
5280 tool_names.contains(&"bash"),
5281 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5282 tool_names
5283 );
5284 }
5285
5286 #[tokio::test]
5287 async fn test_collect_capabilities_tool_count_matches_definitions() {
5288 let registry = CapabilityRegistry::with_builtins();
5291 let collected =
5292 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5293
5294 assert_eq!(
5295 collected.tools.len(),
5296 collected.tool_definitions.len(),
5297 "tool implementations ({}) must match tool definitions ({})",
5298 collected.tools.len(),
5299 collected.tool_definitions.len(),
5300 );
5301 }
5302
5303 #[tokio::test]
5307 async fn test_collect_capabilities_resolves_dependencies() {
5308 let registry = CapabilityRegistry::with_builtins();
5311 let collected =
5312 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5313
5314 assert!(
5316 collected
5317 .applied_ids
5318 .iter()
5319 .any(|id| id == "session_file_system"),
5320 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5321 collected.applied_ids
5322 );
5323
5324 let tool_names: Vec<&str> = collected
5325 .tool_definitions
5326 .iter()
5327 .map(|t| t.name())
5328 .collect();
5329
5330 assert!(
5332 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5333 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5334 tool_names
5335 );
5336
5337 assert_eq!(
5339 collected.tools.len(),
5340 collected.tool_definitions.len(),
5341 "dependency-added tools must have implementations, not just definitions"
5342 );
5343 }
5344
5345 #[test]
5346 fn test_defaults_do_not_include_bash() {
5347 let registry = crate::ToolRegistry::with_defaults();
5350 assert!(
5351 !registry.has("bash"),
5352 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5353 );
5354 }
5355
5356 #[tokio::test]
5363 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5364 let registry = CapabilityRegistry::with_builtins();
5365 let collected =
5366 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5367
5368 let tool_names: Vec<&str> = collected
5369 .tool_definitions
5370 .iter()
5371 .map(|t| t.name())
5372 .collect();
5373 assert!(
5374 tool_names.contains(&"spawn_background"),
5375 "spawn_background must be auto-activated when bashkit_shell (a \
5376 background-capable tool) is in the agent's capability set; got: {:?}",
5377 tool_names
5378 );
5379 assert!(
5380 collected
5381 .applied_ids
5382 .iter()
5383 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5384 "background_execution must be in applied_ids when auto-activated; \
5385 got: {:?}",
5386 collected.applied_ids
5387 );
5388
5389 assert!(
5391 collected
5392 .tools
5393 .iter()
5394 .any(|t| t.name() == "spawn_background"),
5395 "spawn_background tool implementation must be present alongside the \
5396 definition (lockstep contract)"
5397 );
5398 }
5399
5400 #[tokio::test]
5403 async fn test_background_execution_does_not_auto_activate_without_hint() {
5404 let registry = CapabilityRegistry::with_builtins();
5405 let collected =
5407 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5408
5409 let tool_names: Vec<&str> = collected
5410 .tool_definitions
5411 .iter()
5412 .map(|t| t.name())
5413 .collect();
5414 assert!(
5415 !tool_names.contains(&"spawn_background"),
5416 "spawn_background must NOT be activated without a background-capable \
5417 tool; got: {:?}",
5418 tool_names
5419 );
5420 assert!(
5421 !collected
5422 .applied_ids
5423 .iter()
5424 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5425 "background_execution must not appear in applied_ids when no \
5426 background-capable tool is present; got: {:?}",
5427 collected.applied_ids
5428 );
5429 }
5430
5431 #[tokio::test]
5432 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5433 let registry = CapabilityRegistry::with_builtins();
5434 let collected = collect_capabilities(
5435 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5436 ®istry,
5437 &test_ctx(),
5438 )
5439 .await;
5440
5441 assert!(
5442 collected
5443 .tools
5444 .iter()
5445 .any(|tool| tool.name() == "spawn_agent"),
5446 "subagent-only sessions should get the unified spawn_agent adapter"
5447 );
5448 let spawn_agent = collected
5449 .tool_definitions
5450 .iter()
5451 .find(|tool| tool.name() == "spawn_agent")
5452 .expect("spawn_agent definition");
5453 assert_eq!(
5454 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5455 serde_json::json!(["subagent"])
5456 );
5457 assert_eq!(
5458 spawn_agent.concurrency_class(),
5459 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5460 "unified spawn_agent must serialize same-batch spawns before cap checks"
5461 );
5462 }
5463
5464 #[tokio::test]
5465 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5466 let mut registry = CapabilityRegistry::new();
5467 registry.register(AgentHandoffCapability);
5468 let agent_id = crate::typed_id::AgentId::new();
5469 let harness_id = crate::typed_id::HarnessId::new();
5470 let configs = vec![AgentCapabilityConfig {
5471 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5472 config: serde_json::json!({
5473 "targets": [{
5474 "id": "aws_operator",
5475 "name": "AWS Operator",
5476 "agent_id": agent_id,
5477 "harness_id": harness_id
5478 }]
5479 }),
5480 }];
5481 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5482
5483 assert!(
5484 collected
5485 .tools
5486 .iter()
5487 .any(|tool| tool.name() == "spawn_agent"),
5488 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5489 );
5490 let spawn_agent = collected
5491 .tool_definitions
5492 .iter()
5493 .find(|tool| tool.name() == "spawn_agent")
5494 .expect("spawn_agent definition");
5495 assert_eq!(
5496 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5497 serde_json::json!(["agent"])
5498 );
5499 }
5500
5501 #[tokio::test]
5502 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5503 let mut registry = CapabilityRegistry::new();
5504 registry.register(SubagentCapability);
5505 registry.register(AgentHandoffCapability);
5506
5507 let agent_id = crate::typed_id::AgentId::new();
5508 let harness_id = crate::typed_id::HarnessId::new();
5509 let configs = vec![
5510 AgentCapabilityConfig {
5511 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5512 config: serde_json::json!({}),
5513 },
5514 AgentCapabilityConfig {
5515 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5516 config: serde_json::json!({
5517 "targets": [{
5518 "id": "aws_operator",
5519 "name": "AWS Operator",
5520 "agent_id": agent_id,
5521 "harness_id": harness_id
5522 }]
5523 }),
5524 },
5525 ];
5526
5527 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5528 let spawn_agent_defs: Vec<_> = collected
5529 .tool_definitions
5530 .iter()
5531 .filter(|tool| tool.name() == "spawn_agent")
5532 .collect();
5533
5534 assert_eq!(spawn_agent_defs.len(), 1);
5535 let schema = spawn_agent_defs[0].parameters();
5536 assert_eq!(
5537 schema["properties"]["target"]["properties"]["type"]["enum"],
5538 serde_json::json!(["subagent", "agent"])
5539 );
5540 assert!(schema.get("oneOf").is_none());
5543 assert!(schema.get("anyOf").is_none());
5544 assert!(schema.get("allOf").is_none());
5545 assert_eq!(
5546 schema["required"],
5547 serde_json::json!(["name", "instructions", "target"])
5548 );
5549 assert_eq!(
5550 schema["properties"]["target"]["oneOf"],
5551 serde_json::json!([
5552 {
5553 "properties": {"type": {"const": "subagent"}}
5554 },
5555 {
5556 "properties": {"type": {"const": "agent"}},
5557 "required": ["type", "id"]
5558 }
5559 ])
5560 );
5561 }
5562
5563 #[cfg(feature = "a2a")]
5564 #[tokio::test]
5565 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5566 let mut registry = CapabilityRegistry::new();
5567 registry.register(SubagentCapability);
5568 registry.register(A2aAgentDelegationCapability);
5569
5570 let configs = vec![
5571 AgentCapabilityConfig {
5572 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5573 config: serde_json::json!({}),
5574 },
5575 AgentCapabilityConfig {
5576 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5577 config: serde_json::json!({
5578 "agents": [{
5579 "id": "local_app",
5580 "name": "Local App",
5581 "base_url": "https://example.com"
5582 }]
5583 }),
5584 },
5585 ];
5586
5587 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5588 let spawn_agent_defs: Vec<_> = collected
5589 .tool_definitions
5590 .iter()
5591 .filter(|tool| tool.name() == "spawn_agent")
5592 .collect();
5593
5594 assert_eq!(spawn_agent_defs.len(), 1);
5595 assert_eq!(
5596 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5597 serde_json::json!(["subagent", "external_a2a"])
5598 );
5599 assert_eq!(
5600 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5601 serde_json::json!(["background", "foreground"])
5602 );
5603 assert!(
5604 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5605 .as_str()
5606 .expect("mode description")
5607 .contains("wait")
5608 );
5609 let schema = spawn_agent_defs[0].parameters();
5610 assert!(schema.get("oneOf").is_none());
5611 assert_eq!(
5615 schema["required"],
5616 serde_json::json!(["name", "instructions", "target"])
5617 );
5618 assert_eq!(
5619 schema["properties"]["target"]["oneOf"],
5620 serde_json::json!([
5621 {
5622 "properties": {"type": {"const": "subagent"}}
5623 },
5624 {
5625 "properties": {"type": {"const": "external_a2a"}},
5626 "anyOf": [
5627 {"required": ["id"]},
5628 {"required": ["external_agent_id"]}
5629 ]
5630 }
5631 ])
5632 );
5633 }
5634
5635 struct ExistingSpawnAgentCapability;
5636
5637 impl Capability for ExistingSpawnAgentCapability {
5638 fn id(&self) -> &str {
5639 "existing_spawn_agent"
5640 }
5641
5642 fn name(&self) -> &str {
5643 "Existing Spawn Agent"
5644 }
5645
5646 fn description(&self) -> &str {
5647 "Test capability that already owns spawn_agent"
5648 }
5649
5650 fn tools(&self) -> Vec<Box<dyn Tool>> {
5651 vec![Box::new(ExistingSpawnAgentTool)]
5652 }
5653 }
5654
5655 struct ExistingSpawnAgentTool;
5656
5657 #[async_trait]
5658 impl Tool for ExistingSpawnAgentTool {
5659 fn name(&self) -> &str {
5660 "spawn_agent"
5661 }
5662
5663 fn description(&self) -> &str {
5664 "Existing spawn_agent test tool"
5665 }
5666
5667 fn parameters_schema(&self) -> serde_json::Value {
5668 serde_json::json!({
5669 "type": "object",
5670 "properties": {
5671 "target": {
5672 "type": "object",
5673 "properties": {
5674 "type": {"type": "string", "enum": ["external_a2a"]}
5675 },
5676 "required": ["type"]
5677 }
5678 },
5679 "required": ["target"]
5680 })
5681 }
5682
5683 async fn execute(
5684 &self,
5685 _arguments: serde_json::Value,
5686 ) -> crate::tools::ToolExecutionResult {
5687 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5688 }
5689 }
5690
5691 #[tokio::test]
5692 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5693 let mut registry = CapabilityRegistry::new();
5694 registry.register(SubagentCapability);
5695 registry.register(ExistingSpawnAgentCapability);
5696
5697 let collected = collect_capabilities(
5698 &[
5699 SUBAGENTS_CAPABILITY_ID.to_string(),
5700 "existing_spawn_agent".to_string(),
5701 ],
5702 ®istry,
5703 &test_ctx(),
5704 )
5705 .await;
5706
5707 let spawn_agent_defs: Vec<_> = collected
5708 .tool_definitions
5709 .iter()
5710 .filter(|tool| tool.name() == "spawn_agent")
5711 .collect();
5712 assert_eq!(spawn_agent_defs.len(), 1);
5713 assert_eq!(
5714 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5715 serde_json::json!(["external_a2a"])
5716 );
5717 }
5718
5719 #[tokio::test]
5720 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5721 let mut registry = CapabilityRegistry::new();
5722 registry.register(AgentHandoffCapability);
5723 registry.register(ExistingSpawnAgentCapability);
5724
5725 let agent_id = crate::typed_id::AgentId::new();
5726 let harness_id = crate::typed_id::HarnessId::new();
5727 let configs = vec![
5728 AgentCapabilityConfig {
5729 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5730 config: serde_json::json!({
5731 "targets": [{
5732 "id": "aws_operator",
5733 "name": "AWS Operator",
5734 "agent_id": agent_id,
5735 "harness_id": harness_id
5736 }]
5737 }),
5738 },
5739 AgentCapabilityConfig {
5740 capability_ref: CapabilityId::new("existing_spawn_agent"),
5741 config: serde_json::json!({}),
5742 },
5743 ];
5744
5745 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5746
5747 let spawn_agent_defs: Vec<_> = collected
5748 .tool_definitions
5749 .iter()
5750 .filter(|tool| tool.name() == "spawn_agent")
5751 .collect();
5752 assert_eq!(spawn_agent_defs.len(), 1);
5753 assert_eq!(
5754 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5755 serde_json::json!(["external_a2a"])
5756 );
5757 }
5758
5759 #[tokio::test]
5763 async fn test_background_execution_explicit_selection_is_idempotent() {
5764 let registry = CapabilityRegistry::with_builtins();
5765 let collected = collect_capabilities(
5766 &[
5767 "bashkit_shell".to_string(),
5768 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5769 ],
5770 ®istry,
5771 &test_ctx(),
5772 )
5773 .await;
5774
5775 let spawn_background_count = collected
5776 .tool_definitions
5777 .iter()
5778 .filter(|t| t.name() == "spawn_background")
5779 .count();
5780 assert_eq!(
5781 spawn_background_count, 1,
5782 "spawn_background must appear exactly once even when \
5783 background_execution is selected explicitly alongside a \
5784 background-capable tool"
5785 );
5786 let applied_count = collected
5787 .applied_ids
5788 .iter()
5789 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5790 .count();
5791 assert_eq!(
5792 applied_count, 1,
5793 "background_execution must appear exactly once in applied_ids"
5794 );
5795 }
5796
5797 #[test]
5802 fn test_defaults_do_not_include_spawn_background() {
5803 let registry = crate::ToolRegistry::with_defaults();
5804 assert!(
5805 !registry.has("spawn_background"),
5806 "with_defaults() must not include 'spawn_background' — it comes \
5807 from the background_execution capability (EVE-501)"
5808 );
5809 }
5810
5811 #[test]
5816 fn test_capability_features_default_empty() {
5817 let registry = CapabilityRegistry::with_builtins();
5818
5819 let noop = registry.get("noop").unwrap();
5821 assert!(noop.features().is_empty());
5822
5823 let current_time = registry.get("current_time").unwrap();
5824 assert!(current_time.features().is_empty());
5825 }
5826
5827 #[test]
5828 fn test_file_system_capability_features() {
5829 let registry = CapabilityRegistry::with_builtins();
5830
5831 let fs = registry.get("session_file_system").unwrap();
5832 assert_eq!(fs.features(), vec!["file_system"]);
5833 }
5834
5835 #[test]
5836 fn test_bashkit_shell_capability_features() {
5837 let registry = CapabilityRegistry::with_builtins();
5838
5839 let bash = registry.get("bashkit_shell").unwrap();
5840 assert_eq!(bash.features(), vec!["file_system"]);
5841 }
5842
5843 #[test]
5844 fn test_alias_resolves_to_canonical_capability() {
5845 let registry = CapabilityRegistry::with_builtins();
5846
5847 let via_alias = registry.get("virtual_bash").unwrap();
5849 assert_eq!(via_alias.id(), "bashkit_shell");
5850 assert!(registry.has("virtual_bash"));
5851 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5852 assert_eq!(
5853 registry.canonical_id("bashkit_shell"),
5854 Some("bashkit_shell")
5855 );
5856 assert_eq!(registry.canonical_id("nonexistent"), None);
5857 }
5858
5859 #[test]
5860 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5861 let registry = CapabilityRegistry::with_builtins();
5862
5863 let resolved = resolve_dependencies(
5866 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5867 ®istry,
5868 )
5869 .unwrap();
5870 let bash_ids: Vec<_> = resolved
5871 .resolved_ids
5872 .iter()
5873 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5874 .collect();
5875 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5876 assert!(
5878 !resolved
5879 .added_as_dependencies
5880 .contains(&"bashkit_shell".to_string())
5881 );
5882 }
5883
5884 #[test]
5885 fn test_alias_preserves_explicit_config_in_resolution() {
5886 let registry = CapabilityRegistry::with_builtins();
5887
5888 let configs = vec![AgentCapabilityConfig::with_config(
5889 "virtual_bash".to_string(),
5890 serde_json::json!({"key": "value"}),
5891 )];
5892 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5893 let bash = resolved
5894 .iter()
5895 .find(|c| c.capability_id() == "bashkit_shell")
5896 .expect("alias must resolve to canonical bashkit_shell config");
5897 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5898 }
5899
5900 #[test]
5901 fn test_unregister_by_alias_removes_capability_and_aliases() {
5902 let mut registry = CapabilityRegistry::with_builtins();
5903
5904 assert!(registry.unregister("virtual_bash").is_some());
5905 assert!(!registry.has("bashkit_shell"));
5906 assert!(!registry.has("virtual_bash"));
5907 }
5908
5909 #[test]
5910 fn test_session_storage_capability_features() {
5911 let registry = CapabilityRegistry::with_builtins();
5912
5913 let storage = registry.get("session_storage").unwrap();
5914 let features = storage.features();
5915 assert!(features.contains(&"secrets"));
5916 assert!(features.contains(&"key_value"));
5917 }
5918
5919 #[test]
5920 fn test_session_schedule_capability_features() {
5921 let registry = CapabilityRegistry::with_builtins();
5922
5923 let schedule = registry.get("session_schedule").unwrap();
5924 assert_eq!(schedule.features(), vec!["schedules"]);
5925 }
5926
5927 #[test]
5928 fn test_session_sql_database_capability_features() {
5929 let registry = CapabilityRegistry::with_builtins();
5930
5931 let sql = registry.get("session_sql_database").unwrap();
5932 assert_eq!(sql.features(), vec!["sql_database"]);
5933 }
5934
5935 #[test]
5936 fn test_sample_data_capability_features() {
5937 let registry = CapabilityRegistry::with_builtins();
5938
5939 let sample = registry.get("sample_data").unwrap();
5940 assert_eq!(sample.features(), vec!["file_system"]);
5941 }
5942
5943 #[test]
5944 fn test_compute_features_empty() {
5945 let registry = CapabilityRegistry::with_builtins();
5946
5947 let features = compute_features(&[], ®istry);
5948 assert!(features.is_empty());
5949 }
5950
5951 #[test]
5952 fn test_compute_features_single_capability() {
5953 let registry = CapabilityRegistry::with_builtins();
5954
5955 let features = compute_features(&["session_schedule".to_string()], ®istry);
5956 assert_eq!(features, vec!["schedules"]);
5957 }
5958
5959 #[test]
5960 fn test_compute_features_multiple_capabilities() {
5961 let registry = CapabilityRegistry::with_builtins();
5962
5963 let features = compute_features(
5964 &[
5965 "session_file_system".to_string(),
5966 "session_storage".to_string(),
5967 "session_schedule".to_string(),
5968 ],
5969 ®istry,
5970 );
5971 assert!(features.contains(&"file_system".to_string()));
5972 assert!(features.contains(&"secrets".to_string()));
5973 assert!(features.contains(&"key_value".to_string()));
5974 assert!(features.contains(&"schedules".to_string()));
5975 }
5976
5977 #[test]
5978 fn test_compute_features_deduplicates() {
5979 let registry = CapabilityRegistry::with_builtins();
5980
5981 let features = compute_features(
5983 &[
5984 "session_file_system".to_string(),
5985 "bashkit_shell".to_string(),
5986 ],
5987 ®istry,
5988 );
5989 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5990 assert_eq!(file_system_count, 1, "file_system should appear only once");
5991 }
5992
5993 #[test]
5994 fn test_compute_features_includes_dependency_features() {
5995 let registry = CapabilityRegistry::with_builtins();
5996
5997 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5999 assert!(features.contains(&"file_system".to_string()));
6000 }
6001
6002 #[test]
6003 fn test_compute_features_generic_harness_set() {
6004 let registry = CapabilityRegistry::with_builtins();
6005
6006 let features = compute_features(
6008 &[
6009 "session_file_system".to_string(),
6010 "bashkit_shell".to_string(),
6011 "session_storage".to_string(),
6012 "session".to_string(),
6013 "session_schedule".to_string(),
6014 ],
6015 ®istry,
6016 );
6017 assert!(features.contains(&"file_system".to_string()));
6018 assert!(features.contains(&"secrets".to_string()));
6019 assert!(features.contains(&"key_value".to_string()));
6020 assert!(features.contains(&"schedules".to_string()));
6021 }
6022
6023 #[test]
6024 fn test_compute_features_unknown_capability_ignored() {
6025 let registry = CapabilityRegistry::with_builtins();
6026
6027 let features = compute_features(
6028 &["unknown_cap".to_string(), "session_schedule".to_string()],
6029 ®istry,
6030 );
6031 assert_eq!(features, vec!["schedules"]);
6032 }
6033
6034 #[test]
6035 fn test_risk_level_ordering() {
6036 assert!(RiskLevel::Low < RiskLevel::Medium);
6037 assert!(RiskLevel::Medium < RiskLevel::High);
6038 }
6039
6040 #[test]
6041 fn test_risk_level_serde_roundtrip() {
6042 let high = RiskLevel::High;
6043 let json = serde_json::to_string(&high).unwrap();
6044 assert_eq!(json, "\"high\"");
6045 let back: RiskLevel = serde_json::from_str(&json).unwrap();
6046 assert_eq!(back, RiskLevel::High);
6047 }
6048
6049 #[test]
6050 fn test_capability_risk_levels() {
6051 let registry = CapabilityRegistry::with_builtins();
6052
6053 let bash = registry.get("bashkit_shell").unwrap();
6055 assert_eq!(bash.risk_level(), RiskLevel::High);
6056
6057 let fetch = registry.get("web_fetch").unwrap();
6059 assert_eq!(fetch.risk_level(), RiskLevel::High);
6060
6061 let noop = registry.get("noop").unwrap();
6063 assert_eq!(noop.risk_level(), RiskLevel::Low);
6064 }
6065
6066 #[tokio::test]
6071 async fn test_apply_capabilities_openai_tool_search() {
6072 let registry = CapabilityRegistry::with_builtins();
6073 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6074
6075 let applied = apply_capabilities(
6076 base_runtime_agent.clone(),
6077 &["openai_tool_search".to_string()],
6078 ®istry,
6079 &test_ctx(),
6080 )
6081 .await;
6082
6083 assert_eq!(
6085 applied.runtime_agent.system_prompt,
6086 base_runtime_agent.system_prompt
6087 );
6088 assert!(applied.tool_registry.is_empty());
6089 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
6090
6091 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6093 assert!(ts.enabled);
6094 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6095 }
6096
6097 #[tokio::test]
6098 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
6099 let registry = CapabilityRegistry::with_builtins();
6100 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6101
6102 let applied = apply_capabilities(
6103 base_runtime_agent,
6104 &[
6105 "current_time".to_string(),
6106 "openai_tool_search".to_string(),
6107 "test_math".to_string(),
6108 ],
6109 ®istry,
6110 &test_ctx(),
6111 )
6112 .await;
6113
6114 assert!(applied.tool_registry.has("get_current_time"));
6116 assert!(applied.tool_registry.has("add"));
6117 assert!(applied.tool_registry.has("subtract"));
6118 assert!(applied.tool_registry.has("multiply"));
6119 assert!(applied.tool_registry.has("divide"));
6120
6121 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6123 assert!(ts.enabled);
6124 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6125 }
6126
6127 #[tokio::test]
6128 async fn test_collect_capabilities_tool_search_custom_threshold() {
6129 let registry = CapabilityRegistry::with_builtins();
6130
6131 let configs = vec![AgentCapabilityConfig {
6132 capability_ref: CapabilityId::new("openai_tool_search"),
6133 config: serde_json::json!({"threshold": 5}),
6134 }];
6135
6136 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6137
6138 let ts = collected.tool_search.as_ref().unwrap();
6139 assert!(ts.enabled);
6140 assert_eq!(ts.threshold, 5);
6141 }
6142
6143 #[tokio::test]
6144 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
6145 let registry = CapabilityRegistry::with_builtins();
6146
6147 let configs = vec![
6148 AgentCapabilityConfig {
6149 capability_ref: CapabilityId::new("auto_tool_search"),
6150 config: serde_json::json!({"threshold": 2}),
6151 },
6152 AgentCapabilityConfig {
6153 capability_ref: CapabilityId::new("test_math"),
6154 config: serde_json::json!({}),
6155 },
6156 ];
6157
6158 let ctx = test_ctx().with_model("claude-3-5-haiku");
6162 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6163
6164 assert!(
6165 collected.tool_search.is_none(),
6166 "auto_tool_search must not set a hosted config on a non-native model"
6167 );
6168 assert!(
6169 collected
6170 .tools
6171 .iter()
6172 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6173 "auto_tool_search must contribute the client-side tool_search tool"
6174 );
6175 assert!(
6176 !collected.tool_definition_hooks.is_empty(),
6177 "auto_tool_search must contribute a client-side deferral hook"
6178 );
6179
6180 let mut transformed = collected.tool_definitions.clone();
6181 for hook in &collected.tool_definition_hooks {
6182 transformed = hook.transform(transformed);
6183 }
6184 let add_tool = transformed
6185 .iter()
6186 .find(|tool| tool.name() == "add")
6187 .expect("test_math contributes add");
6188 assert!(
6189 add_tool.parameters().get("properties").is_none(),
6190 "generic auto_tool_search must honor the configured threshold"
6191 );
6192 }
6193
6194 #[tokio::test]
6195 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
6196 let registry = CapabilityRegistry::with_builtins();
6197
6198 let configs = vec![AgentCapabilityConfig {
6199 capability_ref: CapabilityId::new("auto_tool_search"),
6200 config: serde_json::json!({"threshold": 7}),
6201 }];
6202
6203 let ctx = test_ctx().with_model("gpt-5.4");
6206 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6207
6208 let ts = collected
6209 .tool_search
6210 .as_ref()
6211 .expect("auto_tool_search must set a hosted config on a native model");
6212 assert!(ts.enabled);
6213 assert_eq!(ts.threshold, 7);
6214 assert!(
6215 !collected
6216 .tools
6217 .iter()
6218 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6219 "hosted mechanism must not contribute the client-side tool_search tool"
6220 );
6221 assert!(
6222 collected.tool_definition_hooks.is_empty(),
6223 "hosted mechanism must not contribute a client-side deferral hook"
6224 );
6225 }
6226
6227 #[tokio::test]
6228 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6229 let registry = CapabilityRegistry::with_builtins();
6230
6231 let configs = vec![AgentCapabilityConfig {
6232 capability_ref: CapabilityId::new("auto_tool_search"),
6233 config: serde_json::json!({"threshold": 9}),
6234 }];
6235
6236 let ctx = test_ctx().with_model("claude-opus-4-8");
6239 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6240
6241 let ts = collected
6242 .tool_search
6243 .as_ref()
6244 .expect("auto_tool_search must set a hosted config on a native Claude model");
6245 assert!(ts.enabled);
6246 assert_eq!(ts.threshold, 9);
6247 assert!(
6248 !collected
6249 .tools
6250 .iter()
6251 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6252 "hosted mechanism must not contribute the client-side tool_search tool"
6253 );
6254 assert!(
6255 collected.tool_definition_hooks.is_empty(),
6256 "hosted mechanism must not contribute a client-side deferral hook"
6257 );
6258 }
6259
6260 #[tokio::test]
6261 async fn test_collect_capabilities_no_tool_search_without_capability() {
6262 let registry = CapabilityRegistry::with_builtins();
6263
6264 let configs = vec![AgentCapabilityConfig {
6265 capability_ref: CapabilityId::new("current_time"),
6266 config: serde_json::json!({}),
6267 }];
6268
6269 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6270
6271 assert!(collected.tool_search.is_none());
6272 }
6273
6274 #[tokio::test]
6275 async fn test_collect_capabilities_tool_search_category_propagation() {
6276 let registry = CapabilityRegistry::with_builtins();
6277
6278 let configs = vec![
6280 AgentCapabilityConfig {
6281 capability_ref: CapabilityId::new("test_math"),
6282 config: serde_json::json!({}),
6283 },
6284 AgentCapabilityConfig {
6285 capability_ref: CapabilityId::new("openai_tool_search"),
6286 config: serde_json::json!({}),
6287 },
6288 ];
6289
6290 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6291
6292 assert!(collected.tool_search.is_some());
6294
6295 for tool_def in &collected.tool_definitions {
6297 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6299 assert!(
6300 tool_def.category().is_some(),
6301 "Tool {} should have a category from its capability",
6302 tool_def.name()
6303 );
6304 }
6305 }
6306 }
6307
6308 #[tokio::test]
6309 async fn test_apply_capabilities_prompt_caching() {
6310 let registry = CapabilityRegistry::with_builtins();
6311 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6312
6313 let applied = apply_capabilities(
6314 base_runtime_agent.clone(),
6315 &["prompt_caching".to_string()],
6316 ®istry,
6317 &test_ctx(),
6318 )
6319 .await;
6320
6321 assert_eq!(
6322 applied.runtime_agent.system_prompt,
6323 base_runtime_agent.system_prompt
6324 );
6325 assert!(applied.tool_registry.is_empty());
6326 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6327
6328 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6329 assert!(prompt_cache.enabled);
6330 assert_eq!(
6331 prompt_cache.strategy,
6332 crate::driver_registry::PromptCacheStrategy::Auto
6333 );
6334 assert!(prompt_cache.gemini_cached_content.is_none());
6335 }
6336
6337 #[tokio::test]
6338 async fn test_apply_capabilities_openrouter_server_tools() {
6339 let registry = CapabilityRegistry::with_builtins();
6340 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6341
6342 let configs = vec![AgentCapabilityConfig {
6343 capability_ref: CapabilityId::new("openrouter_server_tools"),
6344 config: serde_json::json!({
6345 "tools": ["web_search", "datetime"],
6346 "web_search_max_results": 4,
6347 }),
6348 }];
6349
6350 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6351 let routing = collected
6352 .openrouter_routing
6353 .as_ref()
6354 .expect("server tools produce routing config");
6355 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6356 assert_eq!(
6357 kinds,
6358 vec![
6359 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6360 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6361 ]
6362 );
6363
6364 let applied = apply_capabilities(
6367 base_runtime_agent,
6368 &["openrouter_server_tools".to_string()],
6369 ®istry,
6370 &test_ctx(),
6371 )
6372 .await;
6373 assert!(applied.tool_registry.is_empty());
6374 assert!(applied.runtime_agent.openrouter_routing.is_none());
6375 }
6376
6377 #[tokio::test]
6378 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6379 let registry = CapabilityRegistry::with_builtins();
6380
6381 let configs = vec![AgentCapabilityConfig {
6382 capability_ref: CapabilityId::new("prompt_caching"),
6383 config: serde_json::json!({"strategy": "auto"}),
6384 }];
6385
6386 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6387
6388 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6389 assert!(prompt_cache.enabled);
6390 assert_eq!(
6391 prompt_cache.strategy,
6392 crate::driver_registry::PromptCacheStrategy::Auto
6393 );
6394 assert!(prompt_cache.gemini_cached_content.is_none());
6395 }
6396
6397 #[tokio::test]
6398 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6399 let registry = CapabilityRegistry::with_builtins();
6400
6401 let configs = vec![AgentCapabilityConfig {
6402 capability_ref: CapabilityId::new("prompt_caching"),
6403 config: serde_json::json!({
6404 "strategy": "auto",
6405 "gemini_cached_content": "cachedContents/demo-cache"
6406 }),
6407 }];
6408
6409 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6410
6411 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6412 assert_eq!(
6413 prompt_cache.gemini_cached_content.as_deref(),
6414 Some("cachedContents/demo-cache")
6415 );
6416 }
6417
6418 #[tokio::test]
6419 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6420 let registry = CapabilityRegistry::with_builtins();
6421
6422 let collected = collect_capabilities_with_configs(
6424 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6425 ®istry,
6426 &test_ctx(),
6427 )
6428 .await;
6429 assert_eq!(collected.parallel_tool_calls, Some(true));
6430
6431 let collected = collect_capabilities_with_configs(
6433 &[AgentCapabilityConfig {
6434 capability_ref: CapabilityId::new("parallel_tool_calls"),
6435 config: serde_json::json!({"mode": "avoid"}),
6436 }],
6437 ®istry,
6438 &test_ctx(),
6439 )
6440 .await;
6441 assert_eq!(collected.parallel_tool_calls, Some(false));
6442
6443 let collected = collect_capabilities_with_configs(
6445 &[AgentCapabilityConfig {
6446 capability_ref: CapabilityId::new("parallel_tool_calls"),
6447 config: serde_json::json!({"mode": "none"}),
6448 }],
6449 ®istry,
6450 &test_ctx(),
6451 )
6452 .await;
6453 assert_eq!(collected.parallel_tool_calls, None);
6454
6455 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6457 assert_eq!(collected.parallel_tool_calls, None);
6458 }
6459
6460 #[tokio::test]
6461 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6462 let registry = CapabilityRegistry::with_builtins();
6463
6464 let applied = apply_capabilities(
6466 RuntimeAgent::new("p", "gpt-5.2"),
6467 &["parallel_tool_calls".to_string()],
6468 ®istry,
6469 &test_ctx(),
6470 )
6471 .await;
6472 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6473
6474 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6476 base.parallel_tool_calls = Some(false);
6477 let applied = apply_capabilities(
6478 base,
6479 &["parallel_tool_calls".to_string()],
6480 ®istry,
6481 &test_ctx(),
6482 )
6483 .await;
6484 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6485 }
6486
6487 struct SkillContributingCapability;
6492
6493 impl Capability for SkillContributingCapability {
6494 fn id(&self) -> &str {
6495 "contributes_skills"
6496 }
6497 fn name(&self) -> &str {
6498 "Contributes Skills"
6499 }
6500 fn description(&self) -> &str {
6501 "Test capability that contributes skills."
6502 }
6503 fn contribute_skills(&self) -> Vec<SkillContribution> {
6504 vec![
6505 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6506 .with_files(vec![(
6507 "scripts/a.sh".to_string(),
6508 "#!/bin/sh\necho a\n".to_string(),
6509 )]),
6510 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6511 .with_user_invocable(false),
6512 ]
6513 }
6514 }
6515
6516 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6517 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6518 MountSource::InlineFile { content, .. } => content.as_str(),
6519 _ => panic!("Expected InlineFile for SKILL.md"),
6520 }
6521 }
6522
6523 #[tokio::test]
6524 async fn test_contribute_skills_normalized_to_mounts() {
6525 let mut registry = CapabilityRegistry::new();
6526 registry.register(SkillContributingCapability);
6527
6528 let configs = vec![AgentCapabilityConfig {
6529 capability_ref: CapabilityId::new("contributes_skills"),
6530 config: serde_json::json!({}),
6531 }];
6532
6533 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6534
6535 let skill_mounts: Vec<_> = collected
6536 .mounts
6537 .iter()
6538 .filter(|m| m.path.starts_with("/.agents/skills/"))
6539 .collect();
6540 assert_eq!(skill_mounts.len(), 2);
6541
6542 for m in &skill_mounts {
6545 assert!(m.is_readonly());
6546 assert_eq!(m.capability_id, "contributes_skills");
6547 }
6548
6549 let alpha = skill_mounts
6550 .iter()
6551 .find(|m| m.path == "/.agents/skills/alpha-skill")
6552 .expect("alpha-skill mount missing");
6553 match &alpha.source {
6554 MountSource::InlineDirectory { entries } => {
6555 assert!(entries.contains_key("SKILL.md"));
6556 assert!(entries.contains_key("scripts/a.sh"));
6557 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6558 assert_eq!(parsed.name, "alpha-skill");
6559 assert!(parsed.user_invocable);
6560 }
6561 _ => panic!("Expected InlineDirectory"),
6562 }
6563
6564 let beta = skill_mounts
6565 .iter()
6566 .find(|m| m.path == "/.agents/skills/beta-skill")
6567 .expect("beta-skill mount missing");
6568 match &beta.source {
6569 MountSource::InlineDirectory { entries } => {
6570 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6571 assert!(!parsed.user_invocable);
6572 }
6573 _ => panic!("Expected InlineDirectory"),
6574 }
6575 }
6576
6577 #[tokio::test]
6578 async fn test_contribute_skills_default_empty() {
6579 let mut registry = CapabilityRegistry::new();
6582 registry.register(FilterTestCapability { priority: 0 });
6583
6584 let configs = vec![AgentCapabilityConfig {
6585 capability_ref: CapabilityId::new("filter_test"),
6586 config: serde_json::json!({}),
6587 }];
6588
6589 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6590 assert!(
6591 collected
6592 .mounts
6593 .iter()
6594 .all(|m| !m.path.starts_with("/.agents/skills/"))
6595 );
6596 }
6597
6598 struct LocalizedCapability;
6599
6600 impl Capability for LocalizedCapability {
6601 fn id(&self) -> &str {
6602 "localized"
6603 }
6604 fn name(&self) -> &str {
6605 "Localized"
6606 }
6607 fn description(&self) -> &str {
6608 "English description"
6609 }
6610 fn localizations(&self) -> Vec<CapabilityLocalization> {
6611 vec![
6612 CapabilityLocalization {
6613 locale: "en",
6614 name: None,
6615 description: None,
6616 config_description: Some("Controls things."),
6617 config_overlay: None,
6618 },
6619 CapabilityLocalization {
6620 locale: "uk",
6621 name: Some("Локалізована"),
6622 description: Some("Український опис"),
6623 config_description: Some("Керує налаштуваннями."),
6624 config_overlay: None,
6625 },
6626 ]
6627 }
6628 }
6629
6630 #[test]
6631 fn localized_name_falls_back_exact_language_then_base() {
6632 let cap = LocalizedCapability;
6633 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6635 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6636 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6638 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6640 assert_eq!(cap.localized_name(None), "Localized");
6641 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6642 assert_eq!(cap.localized_description(Some("de")), "English description");
6643 }
6644
6645 #[test]
6646 fn describe_schema_resolves_config_description_per_locale() {
6647 let cap = LocalizedCapability;
6648 assert_eq!(
6649 cap.describe_schema(Some("uk-UA")).as_deref(),
6650 Some("Керує налаштуваннями.")
6651 );
6652 assert_eq!(
6654 cap.describe_schema(Some("pl")).as_deref(),
6655 Some("Controls things.")
6656 );
6657 assert_eq!(
6658 cap.describe_schema(None).as_deref(),
6659 Some("Controls things.")
6660 );
6661 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6663 }
6664}