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 prompt_caching;
134mod prompt_canary_guardrail;
135mod research;
136mod sample_data;
137mod self_budget;
138mod session;
139mod session_sandbox;
140mod session_schedule;
141mod session_sql_database;
142mod session_storage;
143mod session_tasks;
144mod skills;
145mod skills_scoped;
146mod stateless_todo_list;
147mod subagents;
148mod system_commands;
149mod test_math;
150mod test_weather;
151mod tool_call_repair;
152mod tool_output_distillation;
153mod tool_output_persistence;
154mod tool_search;
155mod usage_limit_auto_continue;
156pub mod user_hooks;
157mod util;
158#[cfg(feature = "web-fetch")]
159mod web_fetch;
160
161pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
166pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
170#[cfg(feature = "a2a")]
171pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
172#[cfg(feature = "ui-capabilities")]
173pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
174pub use agent_handoff::{
175 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
176};
177pub use agent_instructions::{
178 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
179 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
180 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
181};
182pub use attach_skill::{
183 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
184 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
185 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
186};
187pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
188pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
189pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
190pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
191pub use citation_retrieval::{
192 CITATION_RETRIEVAL_CAPABILITY_ID, CitationRetrievalCapability, CitationRetrievalConfig,
193};
194pub use citation_verification::{
195 CITATION_VERIFICATION_CAPABILITY_ID, CitationVerificationCapability,
196 CitationVerificationConfig, VerificationMode,
197};
198pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
199pub use compaction::{
200 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
201 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
202 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
203 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
204 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
205 build_summarization_prompt, build_summary_message, classify_memory_tiers,
206 compose_summary_with_recent, estimate_tokens, estimate_total_tokens,
207 format_messages_for_summarization, should_compact_proactively,
208};
209pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
210pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
211pub use declarative::{
212 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
213 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
214 declarative_capability_info, hydrate_declarative_capability_config,
215 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
216 plugin_capability_info, validate_declarative_capability_definition,
217};
218pub use delegation_result::{
219 ReportResultTool, ReportTaskProgressTool, report_result_tool_for_child_session,
220 report_task_progress_tool_for_child_session,
221};
222pub use error_disclosure::{
223 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
224};
225pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
226pub use fake_aws::{
227 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
228 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
229 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
230 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
231};
232pub use fake_crm::{
233 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
234 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
235 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
236};
237pub use fake_financial::{
238 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
239 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
240 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
241 FinanceListTransactionsTool,
242};
243pub use fake_warehouse::{
244 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
245 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
246 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
247 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
248};
249pub use file_system::{
250 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
251 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
252};
253pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
254pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
255pub use infinity_context::{
256 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, InfinityContextFilterOnlyCapability,
257 QueryHistoryTool,
258};
259pub use knowledge_base::{
260 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
261 validate_knowledge_base_config,
262};
263pub use knowledge_index::{
264 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
265 validate_knowledge_index_config,
266};
267pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
268pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
269pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
270pub use mcp::{
271 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
272 parse_mcp_capability_id,
273};
274pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
275pub use message_metadata::{
276 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
277 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
278};
279pub use model_scout::{
280 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
281 RouterUpdateProposal, compute_score, rank_results,
282};
283pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
284pub use openai_tool_search::{
285 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
286 model_supports_native_tool_search,
287};
288pub use openrouter_server_tools::{
289 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
290};
291pub use openrouter_workspace::{
292 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
293 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
294 detect_policy_drift,
295};
296#[cfg(feature = "ui-capabilities")]
297pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
298pub use parallel_tool_calls::{
299 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
300 parallel_tool_calls_from_config,
301};
302pub use platform_management::{
303 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
304 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
305 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
306};
307pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
308pub use prompt_canary_guardrail::{
309 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
310 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
311 REASON_CODE_SYSTEM_PROMPT_LEAK,
312};
313pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
314pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
315pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
316pub use session::{
317 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, SessionCapabilityConfig,
318 SessionTitleMutation, WriteSessionTitleTool, session_title_updated_event,
319 update_session_title_with_event,
320};
321pub use session_sandbox::{
322 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
323 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
324};
325pub use session_schedule::{
326 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
327 SessionScheduleCapability,
328};
329pub use session_sql_database::{
330 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
331 SqlSchemaTool,
332};
333pub use session_storage::{
334 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
335 is_internal_session_kv_key,
336};
337pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
338pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
339pub use skills_scoped::{
340 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
341};
342pub use stateless_todo_list::{
343 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
344};
345pub(crate) use subagents::SPAWN_AGENT_CONCURRENCY_CLASS;
346pub use subagents::{SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool, SubagentCapability};
347pub use usage_limit_auto_continue::{
348 AutoContinueConfig, USAGE_LIMIT_AUTO_CONTINUE_CAPABILITY_ID, UsageLimitAutoContinueCapability,
349 resolve_usage_limit_auto_continue,
350};
351pub use bashkit_shell::{
353 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
354};
355pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
356pub use test_math::{
357 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
358};
359pub use test_weather::{
360 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
361};
362pub use tool_call_repair::{
363 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
364 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
365 salvage_tool_arguments, tool_call_repair_capability,
366};
367pub use tool_output_distillation::{
368 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
369};
370pub use tool_output_persistence::{
371 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
372};
373pub use tool_search::{
374 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
375};
376pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
377#[cfg(feature = "web-fetch")]
378pub use web_fetch::{
379 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
380 derive_bot_auth_public_key,
381};
382
383pub struct SystemPromptContext {
393 pub session_id: SessionId,
395 pub locale: Option<String>,
397 pub file_store: Option<Arc<dyn SessionFileSystem>>,
399 pub model: Option<String>,
405}
406
407impl SystemPromptContext {
408 pub fn without_file_store(session_id: SessionId) -> Self {
410 Self {
411 session_id,
412 locale: None,
413 file_store: None,
414 model: None,
415 }
416 }
417
418 pub fn with_model(mut self, model: impl Into<String>) -> Self {
420 self.model = Some(model.into());
421 self
422 }
423}
424
425#[derive(Debug, Clone)]
477pub struct CapabilityLocalization {
478 pub locale: &'static str,
480 pub name: Option<&'static str>,
482 pub description: Option<&'static str>,
484 pub config_description: Option<&'static str>,
489 pub config_overlay: Option<serde_json::Value>,
495}
496
497impl CapabilityLocalization {
498 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
500 Self {
501 locale,
502 name: Some(name),
503 description: Some(description),
504 config_description: None,
505 config_overlay: None,
506 }
507 }
508}
509
510pub fn resolve_localized_field<T>(
514 localizations: &[CapabilityLocalization],
515 locale: Option<&str>,
516 field: impl Fn(&CapabilityLocalization) -> Option<T>,
517) -> Option<T> {
518 let mut candidates: Vec<String> = Vec::new();
519 if let Some(raw) = locale {
520 let normalized = raw.trim().replace('_', "-").to_lowercase();
521 if !normalized.is_empty() {
522 if let Some((language, _)) = normalized.split_once('-') {
523 let language = language.to_string();
524 candidates.push(normalized);
525 candidates.push(language);
526 } else {
527 candidates.push(normalized);
528 }
529 }
530 }
531 candidates.push("en".to_string());
532
533 for candidate in candidates {
534 let hit = localizations
535 .iter()
536 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
537 .and_then(&field);
538 if hit.is_some() {
539 return hit;
540 }
541 }
542 None
543}
544
545#[async_trait]
546pub trait Capability: Send + Sync {
547 fn id(&self) -> &str;
549
550 fn aliases(&self) -> Vec<&'static str> {
559 vec![]
560 }
561
562 fn name(&self) -> &str;
564
565 fn description(&self) -> &str;
567
568 fn localizations(&self) -> Vec<CapabilityLocalization> {
573 vec![]
574 }
575
576 fn localized_name(&self, locale: Option<&str>) -> String {
579 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
580 .unwrap_or_else(|| self.name())
581 .to_string()
582 }
583
584 fn localized_description(&self, locale: Option<&str>) -> String {
586 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
587 .unwrap_or_else(|| self.description())
588 .to_string()
589 }
590
591 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
595 resolve_localized_field(&self.localizations(), locale, |entry| {
596 entry.config_description
597 })
598 .map(str::to_string)
599 }
600
601 fn status(&self) -> CapabilityStatus {
603 CapabilityStatus::Available
604 }
605
606 fn icon(&self) -> Option<&str> {
608 None
609 }
610
611 fn category(&self) -> Option<&str> {
613 None
614 }
615
616 fn is_guardrail(&self) -> bool {
621 false
622 }
623
624 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
635 None
636 }
637
638 fn system_prompt_addition(&self) -> Option<&str> {
658 None
659 }
660
661 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
673 self.system_prompt_addition().map(|addition| {
674 format!(
675 "<capability id=\"{}\">\n{}\n</capability>",
676 self.id(),
677 addition
678 )
679 })
680 }
681
682 fn system_prompt_preview(&self) -> Option<String> {
688 self.system_prompt_addition().map(|s| s.to_string())
689 }
690
691 fn tools(&self) -> Vec<Box<dyn Tool>> {
693 vec![]
694 }
695
696 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
704 self.tools()
705 }
706
707 async fn system_prompt_contribution_with_config(
714 &self,
715 ctx: &SystemPromptContext,
716 _config: &serde_json::Value,
717 ) -> Option<String> {
718 self.system_prompt_contribution(ctx).await
719 }
720
721 fn tool_definitions(&self) -> Vec<ToolDefinition> {
724 self.tools().iter().map(|t| t.to_definition()).collect()
725 }
726
727 fn mounts(&self) -> Vec<MountPoint> {
735 vec![]
736 }
737
738 fn dependencies(&self) -> Vec<&'static str> {
747 vec![]
748 }
749
750 fn features(&self) -> Vec<&'static str> {
765 vec![]
766 }
767
768 fn config_schema(&self) -> Option<serde_json::Value> {
774 None
775 }
776
777 fn config_ui_schema(&self) -> Option<serde_json::Value> {
782 None
783 }
784
785 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
791 Ok(())
792 }
793
794 fn mcp_servers(&self) -> ScopedMcpServers {
800 ScopedMcpServers::default()
801 }
802
803 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
805 self.mcp_servers()
806 }
807
808 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
821 None
822 }
823
824 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
832 None
833 }
834
835 fn llm_error_hook(&self) -> Option<Arc<dyn crate::llm_error_hook::LlmErrorHook>> {
847 None
848 }
849
850 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
865 vec![]
866 }
867
868 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
879 vec![]
880 }
881
882 fn pre_tool_use_hooks_with_config(
887 &self,
888 _config: &serde_json::Value,
889 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
890 self.pre_tool_use_hooks()
891 }
892
893 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
901 vec![]
902 }
903
904 fn post_tool_exec_hooks_with_config(
909 &self,
910 _config: &serde_json::Value,
911 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
912 self.post_tool_exec_hooks()
913 }
914
915 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
924 vec![]
925 }
926
927 fn tool_definition_hooks_with_config(
932 &self,
933 _config: &serde_json::Value,
934 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
935 self.tool_definition_hooks()
936 }
937
938 fn tool_definition_hooks_with_context(
948 &self,
949 _ctx: &SystemPromptContext,
950 config: &serde_json::Value,
951 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
952 self.tool_definition_hooks_with_config(config)
953 }
954
955 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
963 vec![]
964 }
965
966 fn narrate(
980 &self,
981 _tool_def: Option<&ToolDefinition>,
982 tool_call: &ToolCall,
983 phase: crate::tool_narration::ToolNarrationPhase,
984 locale: Option<&str>,
985 ctx: crate::tool_narration::ToolNarrationContext<'_>,
986 ) -> Option<String> {
987 self.tools()
988 .iter()
989 .find(|tool| tool.name() == tool_call.name)
990 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
991 }
992
993 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
1009 vec![]
1010 }
1011
1012 fn user_hooks_with_config(
1018 &self,
1019 _config: &serde_json::Value,
1020 ) -> Vec<crate::user_hook_types::UserHookSpec> {
1021 self.user_hooks()
1022 }
1023
1024 fn risk_level(&self) -> RiskLevel {
1032 RiskLevel::Low
1033 }
1034
1035 fn commands(&self) -> Vec<CommandDescriptor> {
1043 vec![]
1044 }
1045
1046 async fn execute_command(
1060 &self,
1061 request: &ExecuteCommandRequest,
1062 _ctx: &CommandExecutionContext,
1063 ) -> crate::error::Result<CommandResult> {
1064 Err(crate::error::AgentLoopError::config(format!(
1065 "capability {} declared command /{} but does not implement execute_command",
1066 self.id(),
1067 request.name,
1068 )))
1069 }
1070
1071 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1080 vec![]
1081 }
1082
1083 fn contribute_skills(&self) -> Vec<SkillContribution> {
1093 vec![]
1094 }
1095
1096 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1107 vec![]
1108 }
1109
1110 fn post_output_guardrails_with_config(
1122 &self,
1123 _config: &serde_json::Value,
1124 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1125 vec![]
1126 }
1127
1128 fn post_output_annotation_hooks_with_config(
1144 &self,
1145 _config: &serde_json::Value,
1146 ) -> Vec<Arc<dyn crate::annotation_hook::PostGenerationAnnotationHook>> {
1147 vec![]
1148 }
1149
1150 fn citation_verifier_with_config(
1160 &self,
1161 _config: &serde_json::Value,
1162 ) -> Option<Arc<dyn crate::annotation_hook::CitationVerifier>> {
1163 None
1164 }
1165}
1166
1167pub trait ToolDefinitionHook: Send + Sync {
1168 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1169
1170 fn applies_with_native_tool_search(&self) -> bool {
1175 true
1176 }
1177}
1178
1179pub trait ToolCallHook: Send + Sync {
1180 fn narration(
1181 &self,
1182 _tool_def: Option<&ToolDefinition>,
1183 _tool_call: &ToolCall,
1184 _phase: crate::tool_narration::ToolNarrationPhase,
1185 _locale: Option<&str>,
1186 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
1187 ) -> Option<String> {
1188 None
1189 }
1190
1191 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1192 tool_call
1193 }
1194}
1195
1196pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1202
1203impl ToolCallHook for CapabilityNarrationHook {
1204 fn narration(
1205 &self,
1206 tool_def: Option<&ToolDefinition>,
1207 tool_call: &ToolCall,
1208 phase: crate::tool_narration::ToolNarrationPhase,
1209 locale: Option<&str>,
1210 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1211 ) -> Option<String> {
1212 self.0.narrate(tool_def, tool_call, phase, locale, ctx)
1213 }
1214}
1215
1216#[derive(
1220 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1221)]
1222#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1223#[cfg_attr(feature = "openapi", schema(example = "low"))]
1224#[serde(rename_all = "lowercase")]
1225pub enum RiskLevel {
1226 Low,
1228 Medium,
1230 High,
1232}
1233
1234#[derive(Debug, Clone, Serialize, Deserialize)]
1240#[serde(rename_all = "snake_case")]
1241pub enum BlueprintModel {
1242 Fixed(String),
1244 Default(String),
1246 Inherit,
1248}
1249
1250pub struct AgentBlueprint {
1256 pub id: &'static str,
1258 pub name: &'static str,
1260 pub description: &'static str,
1262 pub model: BlueprintModel,
1264 pub system_prompt: &'static str,
1266 pub tools: Vec<Box<dyn Tool>>,
1268 pub max_turns: Option<usize>,
1270 pub config_schema: Option<serde_json::Value>,
1272}
1273
1274impl AgentBlueprint {
1275 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1277 self.tools.iter().map(|t| t.to_definition()).collect()
1278 }
1279}
1280
1281impl std::fmt::Debug for AgentBlueprint {
1282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1283 f.debug_struct("AgentBlueprint")
1284 .field("id", &self.id)
1285 .field("name", &self.name)
1286 .field("model", &self.model)
1287 .field("tool_count", &self.tools.len())
1288 .field("max_turns", &self.max_turns)
1289 .finish()
1290 }
1291}
1292
1293#[derive(Clone)]
1320pub struct CapabilityRegistry {
1321 capabilities: HashMap<String, Arc<dyn Capability>>,
1322 aliases: HashMap<String, String>,
1324}
1325
1326impl CapabilityRegistry {
1327 pub fn new() -> Self {
1329 Self {
1330 capabilities: HashMap::new(),
1331 aliases: HashMap::new(),
1332 }
1333 }
1334
1335 pub fn with_builtins() -> Self {
1340 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1341 }
1342
1343 pub fn runtime_builtins() -> Self {
1354 let mut registry = Self::new();
1355
1356 registry.register(AgentInstructionsCapability);
1357 registry.register(HumanIntentCapability);
1358 registry.register(NoopCapability);
1359 registry.register(CurrentTimeCapability);
1360 registry.register(MessageMetadataCapability);
1361 registry.register(FileSystemCapability);
1362 registry.register(SessionStorageCapability);
1363 registry.register(SessionCapability);
1364 registry.register(StatelessTodoListCapability);
1365 #[cfg(feature = "web-fetch")]
1366 registry.register(WebFetchCapability::from_env());
1367 registry.register(BashkitShellCapability);
1368 registry.register(BtwCapability);
1369 registry.register(InfinityContextCapability);
1370 registry.register(budgeting::BudgetingCapability);
1371 registry.register(SelfBudgetCapability);
1372 registry.register(CompactionCapability);
1373 registry.register(ErrorDisclosureCapability);
1374 registry.register(OpenAiToolSearchCapability::new());
1375 registry.register(ClaudeToolSearchCapability::new());
1376 registry.register(ToolSearchCapability::new());
1377 registry.register(AutoToolSearchCapability::new());
1378 registry.register(PromptCachingCapability::new());
1379 registry.register(ParallelToolCallsCapability);
1380 registry.register(SkillsCapability);
1381 registry.register(SystemCommandsCapability);
1382 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1383 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1384 registry.register(LoopDetectionCapability);
1385 registry.register(ToolCallRepairCapability);
1386 registry.register(PromptCanaryGuardrailCapability);
1387 registry.register(GuardrailsCapability);
1388 registry.register(user_hooks::UserHooksCapability);
1389
1390 let internal_flags = crate::InternalFeatureFlags::from_env();
1391 if internal_flags.lua {
1392 registry.register(LuaCapability);
1393 registry.register(LuaCodeModeCapability);
1394 }
1395
1396 registry
1397 }
1398
1399 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1404 let mut registry = Self::new();
1405
1406 registry.register(AgentInstructionsCapability);
1408 registry.register(HumanIntentCapability);
1409 registry.register(NoopCapability);
1410 registry.register(CurrentTimeCapability);
1411 registry.register(MessageMetadataCapability);
1412 registry.register(ResearchCapability);
1413 registry.register(ModelScoutCapability);
1414 registry.register(OpenRouterWorkspaceCapability);
1415 registry.register(OpenRouterServerToolsCapability);
1416 registry.register(PlatformManagementCapability);
1417 registry.register(FileSystemCapability);
1418 registry.register(MemoryCapability);
1419 registry.register(SessionStorageCapability);
1420 registry.register(SessionCapability);
1421 registry.register(SessionSqlDatabaseCapability);
1422 registry.register(TestMathCapability);
1423 registry.register(TestWeatherCapability);
1424 registry.register(StatelessTodoListCapability);
1425 #[cfg(feature = "web-fetch")]
1426 registry.register(WebFetchCapability::from_env());
1427 registry.register(BashkitShellCapability);
1428 registry.register(BackgroundExecutionCapability);
1429 registry.register(SessionScheduleCapability);
1430 registry.register(BtwCapability);
1431 registry.register(InfinityContextCapability);
1432 registry.register(budgeting::BudgetingCapability);
1433 registry.register(SelfBudgetCapability);
1434 registry.register(CompactionCapability);
1435 registry.register(ErrorDisclosureCapability);
1436
1437 registry.register(OpenAiToolSearchCapability::new());
1439 registry.register(ClaudeToolSearchCapability::new());
1441 registry.register(ToolSearchCapability::new());
1443 registry.register(AutoToolSearchCapability::new());
1445 registry.register(PromptCachingCapability::new());
1446
1447 registry.register(ParallelToolCallsCapability);
1449
1450 registry.register(SkillsCapability);
1452
1453 registry.register(SubagentCapability);
1455
1456 registry.register(SessionTasksCapability);
1458
1459 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1463 registry.register(AgentHandoffCapability);
1464 #[cfg(feature = "a2a")]
1468 registry.register(A2aAgentDelegationCapability);
1469 }
1470
1471 registry.register(SystemCommandsCapability);
1473
1474 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1476 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1477
1478 registry.register(user_hooks::UserHooksCapability);
1481
1482 registry.register(LoopDetectionCapability);
1484
1485 registry.register(UsageLimitAutoContinueCapability);
1492
1493 registry.register(ToolCallRepairCapability);
1497
1498 registry.register(PromptCanaryGuardrailCapability);
1501
1502 registry.register(GuardrailsCapability);
1505
1506 #[cfg(feature = "ui-capabilities")]
1508 {
1509 registry.register(OpenUiCapability);
1510 registry.register(A2UiCapability);
1511 }
1512
1513 registry.register(SampleDataCapability);
1515
1516 registry.register(DataKnowledgeCapability);
1518
1519 registry.register(KnowledgeBaseCapability);
1521
1522 registry.register(KnowledgeIndexCapability);
1524
1525 registry.register(CitationRetrievalCapability);
1527
1528 registry.register(CitationVerificationCapability);
1530
1531 registry.register(FakeWarehouseCapability);
1533 registry.register(FakeAwsCapability);
1534 registry.register(FakeCrmCapability);
1535 registry.register(FakeFinancialCapability);
1536
1537 let internal_flags = crate::InternalFeatureFlags::from_env();
1539 if internal_flags.session_sandbox {
1540 registry.register(SessionSandboxCapability);
1541 }
1542
1543 if internal_flags.lua {
1547 registry.register(LuaCapability);
1548 registry.register(LuaCodeModeCapability);
1551 }
1552 for plugin in inventory::iter::<IntegrationPlugin>() {
1553 if (!plugin.experimental_only || grade.experimental_features_enabled())
1554 && plugin
1555 .feature_flag
1556 .is_none_or(|f| internal_flags.is_enabled(f))
1557 {
1558 registry.register_boxed((plugin.factory)());
1559 }
1560 }
1561
1562 registry
1563 }
1564
1565 pub fn register(&mut self, capability: impl Capability + 'static) {
1567 self.register_arc(Arc::new(capability));
1568 }
1569
1570 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1572 self.register_arc(Arc::from(capability));
1573 }
1574
1575 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1577 let canonical = capability.id().to_string();
1578 for alias in capability.aliases() {
1579 self.aliases.insert(alias.to_string(), canonical.clone());
1580 }
1581 self.capabilities.insert(canonical, capability);
1582 }
1583
1584 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1586 self.capabilities
1587 .get(id)
1588 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1589 }
1590
1591 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1596 if self.capabilities.contains_key(id) {
1597 Some(id)
1598 } else {
1599 self.aliases
1600 .get(id)
1601 .filter(|c| self.capabilities.contains_key(*c))
1602 .map(String::as_str)
1603 }
1604 }
1605
1606 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1608 let canonical = self.canonical_id(id)?.to_string();
1609 let removed = self.capabilities.remove(&canonical);
1610 self.aliases.retain(|_, target| *target != canonical);
1611 removed
1612 }
1613
1614 pub fn has(&self, id: &str) -> bool {
1616 self.get(id).is_some()
1617 }
1618
1619 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1621 self.capabilities.values().collect()
1622 }
1623
1624 pub fn len(&self) -> usize {
1626 self.capabilities.len()
1627 }
1628
1629 pub fn is_empty(&self) -> bool {
1631 self.capabilities.is_empty()
1632 }
1633
1634 pub fn builder() -> CapabilityRegistryBuilder {
1636 CapabilityRegistryBuilder::new()
1637 }
1638
1639 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1643 for cap in self.capabilities.values() {
1644 for bp in cap.agent_blueprints() {
1645 if bp.id == id {
1646 return Some(bp);
1647 }
1648 }
1649 }
1650 None
1651 }
1652
1653 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1657 for (capability_id, cap) in &self.capabilities {
1658 for bp in cap.agent_blueprints() {
1659 if bp.id == id {
1660 return Some((capability_id.clone(), bp));
1661 }
1662 }
1663 }
1664 None
1665 }
1666
1667 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1669 self.capabilities
1670 .values()
1671 .flat_map(|cap| cap.agent_blueprints())
1672 .collect()
1673 }
1674}
1675
1676impl Default for CapabilityRegistry {
1677 fn default() -> Self {
1678 Self::with_builtins()
1679 }
1680}
1681
1682impl std::fmt::Debug for CapabilityRegistry {
1683 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1684 let ids: Vec<_> = self.capabilities.keys().collect();
1685 f.debug_struct("CapabilityRegistry")
1686 .field("capabilities", &ids)
1687 .finish()
1688 }
1689}
1690
1691pub struct CapabilityRegistryBuilder {
1693 registry: CapabilityRegistry,
1694}
1695
1696impl CapabilityRegistryBuilder {
1697 pub fn new() -> Self {
1699 Self {
1700 registry: CapabilityRegistry::new(),
1701 }
1702 }
1703
1704 pub fn with_builtins() -> Self {
1706 Self {
1707 registry: CapabilityRegistry::with_builtins(),
1708 }
1709 }
1710
1711 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1713 self.registry.register(capability);
1714 self
1715 }
1716
1717 pub fn build(self) -> CapabilityRegistry {
1719 self.registry
1720 }
1721}
1722
1723impl Default for CapabilityRegistryBuilder {
1724 fn default() -> Self {
1725 Self::new()
1726 }
1727}
1728
1729pub struct ModelViewContext<'a> {
1735 pub session_id: SessionId,
1736 pub prior_usage: Option<&'a TokenUsage>,
1737}
1738
1739pub trait ModelViewProvider: Send + Sync {
1745 fn apply_model_view(
1746 &self,
1747 messages: Vec<Message>,
1748 config: &serde_json::Value,
1749 context: &ModelViewContext<'_>,
1750 ) -> Vec<Message>;
1751
1752 fn priority(&self) -> i32 {
1753 0
1754 }
1755}
1756
1757pub struct CollectedCapabilities {
1762 pub system_prompt_parts: Vec<String>,
1764 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1766 pub tools: Vec<Box<dyn Tool>>,
1768 pub tool_definitions: Vec<ToolDefinition>,
1770 pub mounts: Vec<MountPoint>,
1772 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1774 pub applied_ids: Vec<String>,
1776 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1778 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1780 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1783 pub parallel_tool_calls: Option<bool>,
1787 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1789 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1791 pub mcp_servers: ScopedMcpServers,
1793 }
1799
1800#[derive(Debug, Clone, PartialEq, Eq)]
1801pub struct SystemPromptAttribution {
1802 pub capability_id: String,
1803 pub content: String,
1804}
1805
1806impl CollectedCapabilities {
1807 pub fn system_prompt_prefix(&self) -> Option<String> {
1810 if self.system_prompt_parts.is_empty() {
1811 None
1812 } else {
1813 Some(self.system_prompt_parts.join("\n\n"))
1814 }
1815 }
1816
1817 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1821 for (provider, config) in &self.message_filter_providers {
1823 provider.apply_filters(query, config);
1824 }
1825 }
1826
1827 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1830 for (provider, config) in &self.message_filter_providers {
1831 provider.post_load(messages, config);
1832 }
1833 }
1834
1835 pub fn has_message_filters(&self) -> bool {
1837 !self.message_filter_providers.is_empty()
1838 }
1839}
1840
1841struct SpawnAgentTargetProvider {
1842 target_type: &'static str,
1843 tool: Box<dyn Tool>,
1844}
1845
1846#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1848#[serde(rename_all = "snake_case")]
1849pub(crate) enum SpawnMode {
1850 Background,
1851 Foreground,
1852}
1853
1854impl SpawnMode {
1855 pub(crate) fn parse(value: &str) -> Option<Self> {
1856 match value {
1857 "background" => Some(Self::Background),
1858 "foreground" => Some(Self::Foreground),
1859 _ => None,
1860 }
1861 }
1862
1863 pub(crate) fn as_str(self) -> &'static str {
1864 match self {
1865 Self::Background => "background",
1866 Self::Foreground => "foreground",
1867 }
1868 }
1869}
1870
1871struct UnifiedSpawnAgentTool {
1872 providers: Vec<SpawnAgentTargetProvider>,
1873}
1874
1875impl UnifiedSpawnAgentTool {
1876 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1877 Self { providers }
1878 }
1879
1880 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1881 self.providers
1882 .iter()
1883 .find(|provider| provider.target_type == target_type)
1884 .map(|provider| provider.tool.as_ref())
1885 }
1886
1887 fn target_types(&self) -> Vec<&'static str> {
1888 ["subagent", "agent", "external_a2a"]
1889 .into_iter()
1890 .filter(|target_type| {
1891 self.providers
1892 .iter()
1893 .any(|provider| provider.target_type == *target_type)
1894 })
1895 .collect()
1896 }
1897
1898 fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
1903 self.target_types()
1904 .into_iter()
1905 .filter_map(|target_type| match target_type {
1906 "subagent" => Some(serde_json::json!({
1907 "properties": {
1908 "type": {"const": "subagent"}
1909 }
1910 })),
1911 "agent" => Some(serde_json::json!({
1912 "properties": {
1913 "type": {"const": "agent"}
1914 },
1915 "required": ["type", "id"]
1916 })),
1917 "external_a2a" => Some(serde_json::json!({
1918 "properties": {
1919 "type": {"const": "external_a2a"}
1920 },
1921 "anyOf": [
1922 {"required": ["id"]},
1923 {"required": ["external_agent_id"]}
1924 ]
1925 })),
1926 _ => None,
1927 })
1928 .collect()
1929 }
1930
1931 }
1941
1942#[async_trait]
1943impl Tool for UnifiedSpawnAgentTool {
1944 fn narrate(
1945 &self,
1946 tool_call: &ToolCall,
1947 phase: crate::tool_narration::ToolNarrationPhase,
1948 locale: Option<&str>,
1949 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1950 ) -> Option<String> {
1951 let target_type = tool_call
1952 .arguments
1953 .get("target")
1954 .and_then(|target| target.get("type"))
1955 .and_then(serde_json::Value::as_str)?;
1956 self.provider_for(target_type)
1957 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1958 }
1959
1960 fn name(&self) -> &str {
1961 "spawn_agent"
1962 }
1963
1964 fn display_name(&self) -> Option<&str> {
1965 Some("Spawn Agent")
1966 }
1967
1968 fn description(&self) -> &str {
1969 "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."
1970 }
1971
1972 fn parameters_schema(&self) -> serde_json::Value {
1973 serde_json::json!({
1974 "type": "object",
1975 "properties": {
1976 "name": {
1977 "type": "string",
1978 "description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
1979 },
1980 "instructions": {
1981 "type": "string",
1982 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
1983 },
1984 "goal": {
1985 "type": "string",
1986 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
1987 },
1988 "lifetime": {
1989 "type": "string",
1990 "enum": ["linked", "detached"],
1991 "default": "linked",
1992 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
1993 },
1994 "seed": {
1995 "type": "string",
1996 "enum": ["fresh", "fork", "workspace"],
1997 "default": "fresh",
1998 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
1999 },
2000 "target": {
2001 "type": "object",
2002 "properties": {
2003 "type": {
2004 "type": "string",
2005 "enum": self.target_types(),
2006 "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."
2007 },
2008 "id": {
2009 "type": "string",
2010 "description": "Configured target id for first-party handoffs or external A2A agents."
2011 },
2012 "external_agent_id": {
2013 "type": "string",
2014 "description": "Configured external A2A agent id."
2015 }
2016 },
2017 "required": ["type"],
2018 "oneOf": self.target_constraint_branches(),
2019 "additionalProperties": false
2020 },
2021 "mode": {
2022 "type": "string",
2023 "enum": ["background", "foreground"],
2024 "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."
2025 },
2026 "blueprint": {
2027 "type": "string",
2028 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
2029 },
2030 "config": {
2031 "type": "object",
2032 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
2033 },
2034 "result_schema": {
2035 "type": "object",
2036 "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."
2037 },
2038 "message_schema": {
2039 "type": "object",
2040 "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."
2041 },
2042 "public_context": {
2043 "type": "object",
2044 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
2045 },
2046 "wait_timeout_secs": {
2047 "type": "integer",
2048 "minimum": 1,
2049 "maximum": 86400,
2050 "description": "External-A2A-only foreground timeout."
2051 },
2052 "wake_on_completion": {
2053 "type": "boolean",
2054 "description": "External-A2A-only control for background completion wake-ups."
2055 }
2056 },
2057 "required": ["name", "instructions", "target"],
2058 "additionalProperties": false
2059 })
2060 }
2061
2062 fn hints(&self) -> crate::tool_types::ToolHints {
2063 let mut hints = crate::tool_types::ToolHints::default()
2064 .with_long_running(true)
2065 .with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
2066 if self.provider_for("external_a2a").is_some() {
2067 hints = hints.with_open_world(true);
2068 }
2069 hints
2070 }
2071
2072 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
2073 ToolExecutionResult::tool_error(
2074 "spawn_agent requires context. This tool must be executed with session context.",
2075 )
2076 }
2077
2078 async fn execute_with_context(
2079 &self,
2080 arguments: serde_json::Value,
2081 context: &ToolContext,
2082 ) -> ToolExecutionResult {
2083 let target_type = match arguments
2084 .get("target")
2085 .and_then(|target| target.get("type"))
2086 .and_then(serde_json::Value::as_str)
2087 {
2088 Some(target_type) => target_type,
2089 None => {
2090 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
2091 }
2092 };
2093
2094 let Some(provider) = self.provider_for(target_type) else {
2095 let supported = self.target_types().join(", ");
2096 return ToolExecutionResult::tool_error(format!(
2097 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
2098 ));
2099 };
2100 if target_type == "external_a2a"
2101 && arguments
2102 .get("lifetime")
2103 .and_then(serde_json::Value::as_str)
2104 .is_some_and(|value| value == "detached")
2105 {
2106 return ToolExecutionResult::tool_error(
2107 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2108 );
2109 }
2110 if target_type == "external_a2a"
2111 && arguments
2112 .get("message_schema")
2113 .is_some_and(|schema| !schema.is_null())
2114 {
2115 return ToolExecutionResult::tool_error(
2116 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2117 );
2118 }
2119
2120 provider.execute_with_context(arguments, context).await
2121 }
2122
2123 fn requires_context(&self) -> bool {
2124 true
2125 }
2126}
2127
2128pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2133 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2134 return base_system_prompt.to_string();
2135 };
2136
2137 if base_system_prompt.is_empty() {
2138 return additions.to_string();
2139 }
2140
2141 if base_system_prompt.contains("<system-prompt>") {
2142 format!("{base_system_prompt}\n\n{additions}")
2143 } else {
2144 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2145 }
2146}
2147
2148pub struct CollectedMessageFilters {
2155 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2157}
2158
2159pub struct CollectedModelViewProviders {
2161 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2163}
2164
2165impl CollectedMessageFilters {
2171 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2173 for (provider, config) in &self.message_filter_providers {
2174 provider.apply_filters(query, config);
2175 }
2176 }
2177
2178 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2180 for (provider, config) in &self.message_filter_providers {
2181 provider.post_load(messages, config);
2182 }
2183 }
2184}
2185
2186impl CollectedModelViewProviders {
2187 pub fn apply_model_view(
2189 &self,
2190 mut messages: Vec<Message>,
2191 context: &ModelViewContext<'_>,
2192 ) -> Vec<Message> {
2193 for (provider, config) in &self.model_view_providers {
2194 messages = provider.apply_model_view(messages, config, context);
2195 }
2196 messages
2197 }
2198}
2199
2200fn compaction_is_enabled(
2206 capability_configs: &[AgentCapabilityConfig],
2207 registry: &CapabilityRegistry,
2208) -> bool {
2209 capability_configs.iter().any(|cap_config| {
2210 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2211 && registry
2212 .get(cap_config.capability_ref.as_str())
2213 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2214 })
2215}
2216
2217fn message_filter_config_for(
2226 cap_id: &str,
2227 base: &serde_json::Value,
2228 compaction_on: bool,
2229) -> serde_json::Value {
2230 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2231 return base.clone();
2232 }
2233 let mut config = base.clone();
2234 match config.as_object_mut() {
2235 Some(map) => {
2236 map.insert(
2237 "compaction_active".to_string(),
2238 serde_json::Value::Bool(true),
2239 );
2240 }
2241 None => {
2242 config = serde_json::json!({ "compaction_active": true });
2243 }
2244 }
2245 config
2246}
2247
2248pub fn collect_message_filters_only(
2254 capability_configs: &[AgentCapabilityConfig],
2255 registry: &CapabilityRegistry,
2256) -> CollectedMessageFilters {
2257 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2258 Vec::new();
2259 let compaction_on = compaction_is_enabled(capability_configs, registry);
2260
2261 for cap_config in capability_configs {
2262 let cap_id = cap_config.capability_ref.as_str();
2263 if let Some(capability) = registry.get(cap_id) {
2264 if capability.status() != CapabilityStatus::Available {
2265 continue;
2266 }
2267 let effective: &dyn Capability = capability
2270 .resolve_for_model(None)
2271 .unwrap_or_else(|| capability.as_ref());
2272 if let Some(provider) = effective.message_filter_provider() {
2273 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2274 message_filter_providers.push((provider, config));
2275 }
2276 }
2277 }
2278
2279 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2280
2281 CollectedMessageFilters {
2282 message_filter_providers,
2283 }
2284}
2285
2286pub fn collect_model_view_providers(
2293 capability_configs: &[AgentCapabilityConfig],
2294 registry: &CapabilityRegistry,
2295 model: Option<&str>,
2296) -> CollectedModelViewProviders {
2297 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2298
2299 for cap_config in capability_configs {
2300 let cap_id = cap_config.capability_ref.as_str();
2301 if let Some(capability) = registry.get(cap_id) {
2302 if capability.status() != CapabilityStatus::Available {
2303 continue;
2304 }
2305 let effective: &dyn Capability = capability
2306 .resolve_for_model(model)
2307 .unwrap_or_else(|| capability.as_ref());
2308 if let Some(provider) = effective.model_view_provider() {
2309 model_view_providers.push((provider, cap_config.config.clone()));
2310 }
2311 }
2312 }
2313
2314 model_view_providers.sort_by_key(|(p, _)| p.priority());
2315
2316 CollectedModelViewProviders {
2317 model_view_providers,
2318 }
2319}
2320
2321pub fn collect_dynamic_facts(
2327 capability_configs: &[AgentCapabilityConfig],
2328 registry: &CapabilityRegistry,
2329 model: Option<&str>,
2330 ctx: &FactsContext,
2331) -> Vec<Fact> {
2332 let mut dynamic = Vec::new();
2333 for cap_config in capability_configs {
2334 let cap_id = cap_config.capability_ref.as_str();
2335 if let Some(capability) = registry.get(cap_id) {
2336 if capability.status() != CapabilityStatus::Available {
2337 continue;
2338 }
2339 let effective: &dyn Capability = capability
2340 .resolve_for_model(model)
2341 .unwrap_or_else(|| capability.as_ref());
2342 for fact in effective.facts(&cap_config.config, ctx) {
2343 if fact.volatility == Volatility::Dynamic {
2344 dynamic.push(fact);
2345 }
2346 }
2347 }
2348 }
2349 dynamic
2350}
2351
2352pub fn collect_capability_mcp_servers(
2353 capability_configs: &[AgentCapabilityConfig],
2354 registry: &CapabilityRegistry,
2355) -> ScopedMcpServers {
2356 let mut servers = ScopedMcpServers::default();
2357
2358 for cap_config in capability_configs {
2359 let cap_id = cap_config.capability_ref.as_str();
2360 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2363 if let Ok(definition) =
2364 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2365 {
2366 if definition.status != CapabilityStatus::Available {
2367 continue;
2368 }
2369 if let Some(contributed) = definition.mcp_servers {
2370 servers = merge_scoped_mcp_servers(&servers, &contributed);
2371 }
2372 }
2373 continue;
2374 }
2375 if let Some(capability) = registry.get(cap_id) {
2376 if capability.status() != CapabilityStatus::Available {
2377 continue;
2378 }
2379 servers = merge_scoped_mcp_servers(
2380 &servers,
2381 &capability.mcp_servers_with_config(&cap_config.config),
2382 );
2383 }
2384 }
2385
2386 servers
2387}
2388
2389pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2396
2397#[derive(Debug, Clone, PartialEq, Eq)]
2399pub enum DependencyError {
2400 CircularDependency {
2402 capability_id: String,
2404 chain: Vec<String>,
2406 },
2407 TooManyCapabilities {
2409 count: usize,
2411 max: usize,
2413 },
2414}
2415
2416impl std::fmt::Display for DependencyError {
2417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2418 match self {
2419 DependencyError::CircularDependency {
2420 capability_id,
2421 chain,
2422 } => {
2423 write!(
2424 f,
2425 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2426 capability_id,
2427 chain.join(" -> "),
2428 capability_id
2429 )
2430 }
2431 DependencyError::TooManyCapabilities { count, max } => {
2432 write!(
2433 f,
2434 "Too many capabilities after resolution: {} (max: {})",
2435 count, max
2436 )
2437 }
2438 }
2439 }
2440}
2441
2442impl std::error::Error for DependencyError {}
2443
2444#[derive(Debug, Clone)]
2446pub struct ResolvedCapabilities {
2447 pub resolved_ids: Vec<String>,
2450 pub added_as_dependencies: Vec<String>,
2452 pub user_selected: Vec<String>,
2454}
2455
2456pub fn resolve_dependencies(
2476 selected_ids: &[String],
2477 registry: &CapabilityRegistry,
2478) -> Result<ResolvedCapabilities, DependencyError> {
2479 use std::collections::HashSet;
2480
2481 let user_selected: HashSet<String> = selected_ids
2483 .iter()
2484 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2485 .collect();
2486 let mut resolved: Vec<String> = Vec::new();
2487 let mut resolved_set: HashSet<String> = HashSet::new();
2488 let mut added_as_dependencies: Vec<String> = Vec::new();
2489
2490 for cap_id in selected_ids {
2492 resolve_single_capability(
2493 cap_id,
2494 registry,
2495 &mut resolved,
2496 &mut resolved_set,
2497 &mut added_as_dependencies,
2498 &user_selected,
2499 &mut Vec::new(), )?;
2501 }
2502
2503 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2505 return Err(DependencyError::TooManyCapabilities {
2506 count: resolved.len(),
2507 max: MAX_RESOLVED_CAPABILITIES,
2508 });
2509 }
2510
2511 Ok(ResolvedCapabilities {
2512 resolved_ids: resolved,
2513 added_as_dependencies,
2514 user_selected: selected_ids.to_vec(),
2515 })
2516}
2517
2518pub fn resolve_capability_configs(
2523 selected_configs: &[AgentCapabilityConfig],
2524 registry: &CapabilityRegistry,
2525) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2526 let mut selected_ids: Vec<String> = Vec::new();
2527 for config in selected_configs {
2528 if (is_declarative_capability(config.capability_id())
2531 || is_plugin_capability(config.capability_id()))
2532 && let Ok(definition) =
2533 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2534 {
2535 selected_ids.extend(definition.dependencies);
2536 }
2537 selected_ids.push(config.capability_id().to_string());
2538 }
2539 let resolved = resolve_dependencies(&selected_ids, registry)?;
2540
2541 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2544 .iter()
2545 .map(|config| {
2546 let id = config.capability_id();
2547 let id = registry.canonical_id(id).unwrap_or(id);
2548 (id.to_string(), config.config.clone())
2549 })
2550 .collect();
2551
2552 Ok(resolved
2553 .resolved_ids
2554 .into_iter()
2555 .map(|capability_id| {
2556 explicit_configs
2557 .get(&capability_id)
2558 .cloned()
2559 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2560 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2561 })
2562 .collect())
2563}
2564
2565fn resolve_single_capability(
2567 cap_id: &str,
2568 registry: &CapabilityRegistry,
2569 resolved: &mut Vec<String>,
2570 resolved_set: &mut std::collections::HashSet<String>,
2571 added_as_dependencies: &mut Vec<String>,
2572 user_selected: &std::collections::HashSet<String>,
2573 visiting: &mut Vec<String>,
2574) -> Result<(), DependencyError> {
2575 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2579
2580 if resolved_set.contains(cap_id) {
2582 return Ok(());
2583 }
2584
2585 if visiting.contains(&cap_id.to_string()) {
2587 return Err(DependencyError::CircularDependency {
2588 capability_id: cap_id.to_string(),
2589 chain: visiting.clone(),
2590 });
2591 }
2592
2593 let capability = match registry.get(cap_id) {
2595 Some(cap) => cap,
2596 None => {
2597 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2601 && !resolved_set.contains(cap_id)
2602 {
2603 resolved.push(cap_id.to_string());
2604 resolved_set.insert(cap_id.to_string());
2605 if !user_selected.contains(cap_id) {
2606 added_as_dependencies.push(cap_id.to_string());
2607 }
2608 }
2609 return Ok(());
2610 }
2611 };
2612
2613 visiting.push(cap_id.to_string());
2615
2616 for dep_id in capability.dependencies() {
2618 resolve_single_capability(
2619 dep_id,
2620 registry,
2621 resolved,
2622 resolved_set,
2623 added_as_dependencies,
2624 user_selected,
2625 visiting,
2626 )?;
2627 }
2628
2629 visiting.pop();
2631
2632 if !resolved_set.contains(cap_id) {
2634 resolved.push(cap_id.to_string());
2635 resolved_set.insert(cap_id.to_string());
2636
2637 if !user_selected.contains(cap_id) {
2639 added_as_dependencies.push(cap_id.to_string());
2640 }
2641 }
2642
2643 Ok(())
2644}
2645
2646pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2651 use std::collections::HashSet;
2652
2653 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2654 Ok(resolved) => resolved.resolved_ids,
2655 Err(_) => capability_ids.to_vec(),
2656 };
2657
2658 let mut seen = HashSet::new();
2659 let mut features = Vec::new();
2660 for cap_id in &resolved_ids {
2661 if let Some(cap) = registry.get(cap_id) {
2662 for feature in cap.features() {
2663 if seen.insert(feature) {
2664 features.push(feature.to_string());
2665 }
2666 }
2667 }
2668 }
2669 features
2670}
2671
2672pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2675 registry
2676 .get(cap_id)
2677 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2678 .unwrap_or_default()
2679}
2680
2681pub async fn collect_capabilities(
2697 capability_ids: &[String],
2698 registry: &CapabilityRegistry,
2699 ctx: &SystemPromptContext,
2700) -> CollectedCapabilities {
2701 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2704 Ok(resolved) => resolved.resolved_ids,
2705 Err(e) => {
2706 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2707 capability_ids.to_vec()
2708 }
2709 };
2710
2711 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2713 .iter()
2714 .map(|id| AgentCapabilityConfig {
2715 capability_ref: CapabilityId::new(id),
2716 config: serde_json::Value::Object(serde_json::Map::new()),
2717 })
2718 .collect();
2719
2720 collect_capabilities_with_configs(&configs, registry, ctx).await
2721}
2722
2723pub async fn collect_capabilities_with_configs(
2734 capability_configs: &[AgentCapabilityConfig],
2735 registry: &CapabilityRegistry,
2736 ctx: &SystemPromptContext,
2737) -> CollectedCapabilities {
2738 let mut system_prompt_parts: Vec<String> = Vec::new();
2739 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2740 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2741 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2742 let mut mounts: Vec<MountPoint> = Vec::new();
2743 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2744 Vec::new();
2745 let mut applied_ids: Vec<String> = Vec::new();
2746 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2747 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2748 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2749 let mut parallel_tool_calls: Option<bool> = None;
2750 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2751 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2752 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2755 let mut mcp_servers = ScopedMcpServers::default();
2756 let mut static_facts: Vec<Fact> = Vec::new();
2760 let mut has_dynamic_facts = false;
2761 let facts_ctx = FactsContext::new(ctx.session_id);
2762 let compaction_on = compaction_is_enabled(capability_configs, registry);
2763 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2764 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2765
2766 for cap_config in capability_configs {
2767 let cap_id = cap_config.capability_ref.as_str();
2768 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2773 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2774 cap_config.config.clone(),
2775 ) {
2776 Ok(definition) => {
2777 if definition.status != CapabilityStatus::Available {
2778 continue;
2779 }
2780
2781 if let Some(prompt) = definition.system_prompt.as_deref() {
2782 let contribution =
2783 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2784 system_prompt_attributions.push(SystemPromptAttribution {
2785 capability_id: cap_id.to_string(),
2786 content: contribution.clone(),
2787 });
2788 system_prompt_parts.push(contribution);
2789 }
2790
2791 mounts.extend(definition.mounts(cap_id));
2792 if let Some(ref servers) = definition.mcp_servers {
2793 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2794 }
2795 for skill in definition.skill_contributions() {
2796 mounts.push(skill.to_mount(cap_id));
2797 }
2798
2799 applied_ids.push(cap_id.to_string());
2800 }
2801 Err(error) => {
2802 tracing::warn!(
2803 capability_id = %cap_id,
2804 error = %error,
2805 "Skipping invalid declarative/plugin capability config"
2806 );
2807 }
2808 }
2809 continue;
2810 }
2811 if let Some(capability) = registry.get(cap_id) {
2812 if capability.status() != CapabilityStatus::Available {
2814 continue;
2815 }
2816
2817 let effective: &dyn Capability =
2829 match capability.resolve_for_model(ctx.model.as_deref()) {
2830 Some(inner) => inner,
2831 None => capability.as_ref(),
2832 };
2833 let effective_id = effective.id();
2834 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2835 agent_handoff_spawn_config = Some(cap_config.config.clone());
2836 }
2837
2838 if let Some(contribution) = effective
2840 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2841 .await
2842 {
2843 system_prompt_attributions.push(SystemPromptAttribution {
2844 capability_id: cap_id.to_string(),
2845 content: contribution.clone(),
2846 });
2847 system_prompt_parts.push(contribution);
2848 }
2849
2850 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2855 match fact.volatility {
2856 Volatility::Static => static_facts.push(fact),
2857 Volatility::Dynamic => has_dynamic_facts = true,
2858 }
2859 }
2860
2861 for tool in effective.tools_with_config(&cap_config.config) {
2863 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2864 spawn_agent_providers.push(SpawnAgentTargetProvider {
2865 target_type: "external_a2a",
2866 tool,
2867 });
2868 } else {
2869 tools.push(tool);
2870 }
2871 }
2872 tool_definition_hooks
2873 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2874 tool_call_hooks.extend(effective.tool_call_hooks());
2875 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2877 let cap_category = effective.category();
2882 for def in effective.tool_definitions() {
2883 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2884 continue;
2885 }
2886 let def = match (def.category(), cap_category) {
2887 (None, Some(cat)) => def.with_category(cat),
2888 _ => def,
2889 }
2890 .with_capability_attribution(cap_id, Some(capability.name()));
2891 tool_definitions.push(def);
2892 }
2893
2894 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2902 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2903 {
2904 let threshold = cap_config
2906 .config
2907 .get("threshold")
2908 .and_then(|v| v.as_u64())
2909 .map(|v| v as usize)
2910 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2911 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2912 enabled: true,
2913 threshold,
2914 });
2915 }
2916
2917 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2918 let strategy = cap_config
2919 .config
2920 .get("strategy")
2921 .and_then(|v| v.as_str())
2922 .map(|value| match value {
2923 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2924 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2925 })
2926 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2927 let gemini_cached_content = cap_config
2928 .config
2929 .get("gemini_cached_content")
2930 .and_then(|v| v.as_str())
2931 .map(str::to_string);
2932 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2933 enabled: true,
2934 strategy,
2935 gemini_cached_content,
2936 });
2937 }
2938
2939 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2940 parallel_tool_calls =
2941 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2942 }
2943
2944 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2945 let server_tools =
2946 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2947 if !server_tools.is_empty() {
2948 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2949 server_tools,
2950 ..Default::default()
2951 });
2952 }
2953 }
2954
2955 mounts.extend(effective.mounts());
2957
2958 mcp_servers = merge_scoped_mcp_servers(
2959 &mcp_servers,
2960 &effective.mcp_servers_with_config(&cap_config.config),
2961 );
2962
2963 for skill in effective.contribute_skills() {
2967 mounts.push(skill.to_mount(cap_id));
2968 }
2969
2970 if let Some(provider) = effective.message_filter_provider() {
2972 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2973 message_filter_providers.push((provider, config));
2974 }
2975
2976 applied_ids.push(cap_id.to_string());
2977 }
2978 }
2979
2980 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
2985 spawn_agent_providers.push(SpawnAgentTargetProvider {
2986 target_type: "subagent",
2987 tool: Box::new(SpawnSubagentAsAgentTool),
2988 });
2989 }
2990 if let Some(config) = agent_handoff_spawn_config.as_ref() {
2991 spawn_agent_providers.push(SpawnAgentTargetProvider {
2992 target_type: "agent",
2993 tool: Box::new(SpawnAgentHandoffTool::new(config)),
2994 });
2995 }
2996 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
2997 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
2998 let def = tool
2999 .to_definition()
3000 .with_category("Orchestration")
3001 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
3002 tools.push(Box::new(tool));
3003 tool_definitions.push(def);
3004 }
3005
3006 if !applied_ids
3018 .iter()
3019 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
3020 && tool_definitions
3021 .iter()
3022 .any(|def| def.hints().supports_background == Some(true))
3023 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
3024 && bg_cap.status() == CapabilityStatus::Available
3025 {
3026 tools.extend(bg_cap.tools());
3027 let cap_category = bg_cap.category();
3028 for def in bg_cap.tool_definitions() {
3029 let def = match (def.category(), cap_category) {
3030 (None, Some(cat)) => def.with_category(cat),
3031 _ => def,
3032 }
3033 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
3034 tool_definitions.push(def);
3035 }
3036 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
3037 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
3038 }
3039
3040 if let Some(block) = facts::render_facts_block(&static_facts) {
3045 system_prompt_attributions.push(SystemPromptAttribution {
3046 capability_id: "facts".to_string(),
3047 content: block.clone(),
3048 });
3049 system_prompt_parts.push(block);
3050 }
3051 if has_dynamic_facts {
3052 system_prompt_attributions.push(SystemPromptAttribution {
3053 capability_id: "facts".to_string(),
3054 content: FACTS_DYNAMIC_NOTE.to_string(),
3055 });
3056 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
3057 }
3058
3059 tool_call_hooks.extend(narration_hooks);
3063
3064 message_filter_providers.sort_by_key(|(p, _)| p.priority());
3066
3067 CollectedCapabilities {
3068 system_prompt_parts,
3069 system_prompt_attributions,
3070 tools,
3071 tool_definitions,
3072 mounts,
3073 message_filter_providers,
3074 applied_ids,
3075 tool_search,
3076 prompt_cache,
3077 openrouter_routing,
3078 parallel_tool_calls,
3079 tool_definition_hooks,
3080 tool_call_hooks,
3081 mcp_servers,
3082 }
3083}
3084
3085pub struct AppliedCapabilities {
3091 pub runtime_agent: RuntimeAgent,
3093 pub tool_registry: ToolRegistry,
3095 pub applied_ids: Vec<String>,
3097}
3098
3099pub async fn apply_capabilities(
3136 base_runtime_agent: RuntimeAgent,
3137 capability_ids: &[String],
3138 registry: &CapabilityRegistry,
3139 ctx: &SystemPromptContext,
3140) -> AppliedCapabilities {
3141 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3142
3143 let final_system_prompt = compose_system_prompt(
3145 &base_runtime_agent.system_prompt,
3146 collected.system_prompt_prefix().as_deref(),
3147 );
3148
3149 let mut tool_registry = ToolRegistry::new();
3151 for tool in collected.tools {
3152 tool_registry.register_boxed(tool);
3153 }
3154
3155 let mut tools = collected.tool_definitions;
3157 for hook in &collected.tool_definition_hooks {
3158 tools = hook.transform(tools);
3159 }
3160
3161 let runtime_agent = RuntimeAgent {
3162 system_prompt: final_system_prompt,
3163 model: base_runtime_agent.model,
3164 tools,
3165 max_iterations: base_runtime_agent.max_iterations,
3166 temperature: base_runtime_agent.temperature,
3167 max_tokens: base_runtime_agent.max_tokens,
3168 tool_search: collected.tool_search,
3169 prompt_cache: collected.prompt_cache,
3170 openrouter_routing: collected.openrouter_routing,
3171 network_access: base_runtime_agent.network_access,
3172 parallel_tool_calls: base_runtime_agent
3175 .parallel_tool_calls
3176 .or(collected.parallel_tool_calls),
3177 };
3178
3179 AppliedCapabilities {
3180 runtime_agent,
3181 tool_registry,
3182 applied_ids: collected.applied_ids,
3183 }
3184}
3185
3186#[cfg(test)]
3191mod tests {
3192 use super::*;
3193 use crate::typed_id::SessionId;
3194 use std::collections::BTreeSet;
3195 use uuid::Uuid;
3196
3197 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3199
3200 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3201 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3202 }
3203
3204 fn test_ctx() -> SystemPromptContext {
3206 SystemPromptContext::without_file_store(SessionId::new())
3207 }
3208
3209 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3211 let mut ids = [
3212 "agent_instructions",
3213 "human_intent",
3214 "budgeting",
3215 "self_budget",
3216 "noop",
3217 "current_time",
3218 "research",
3219 "platform_management",
3220 "session_file_system",
3221 "session_storage",
3222 "session",
3223 "session_sql_database",
3224 "test_math",
3225 "test_weather",
3226 "stateless_todo_list",
3227 "web_fetch",
3228 "bashkit_shell",
3229 "background_execution",
3230 "session_schedule",
3231 "btw",
3232 "infinity_context",
3233 "compaction",
3234 "memory",
3235 "message_metadata",
3236 "openai_tool_search",
3237 "claude_tool_search",
3238 "tool_search",
3239 "auto_tool_search",
3240 "prompt_caching",
3241 "parallel_tool_calls",
3242 "session_tasks",
3243 "skills",
3244 "subagents",
3245 "system_commands",
3246 "sample_data",
3247 "data_knowledge",
3248 "knowledge_base",
3249 "knowledge_index",
3250 "citation_retrieval",
3251 "citation_verification",
3252 "tool_output_persistence",
3253 "tool_output_distillation",
3254 "fake_warehouse",
3255 "fake_aws",
3256 "fake_crm",
3257 "fake_financial",
3258 "loop_detection",
3259 "usage_limit_auto_continue",
3260 "tool_call_repair",
3261 "error_disclosure",
3262 "prompt_canary_guardrail",
3263 "guardrails",
3264 "user_hooks",
3265 "model_scout",
3266 "openrouter_workspace",
3267 "openrouter_server_tools",
3268 ]
3269 .into_iter()
3270 .collect::<BTreeSet<_>>();
3271 if cfg!(feature = "ui-capabilities") {
3272 ids.insert("openui");
3273 ids.insert("a2ui");
3274 }
3275 ids
3276 }
3277
3278 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3280 let mut ids = [
3281 "agent_instructions",
3282 "human_intent",
3283 "budgeting",
3284 "self_budget",
3285 "noop",
3286 "current_time",
3287 "session_file_system",
3288 "session_storage",
3289 "session",
3290 "stateless_todo_list",
3291 "bashkit_shell",
3292 "btw",
3293 "infinity_context",
3294 "compaction",
3295 "message_metadata",
3296 "openai_tool_search",
3297 "claude_tool_search",
3298 "tool_search",
3299 "auto_tool_search",
3300 "prompt_caching",
3301 "parallel_tool_calls",
3302 "skills",
3303 "system_commands",
3304 "tool_output_persistence",
3305 "tool_output_distillation",
3306 "loop_detection",
3307 "tool_call_repair",
3308 "error_disclosure",
3309 "prompt_canary_guardrail",
3310 "guardrails",
3311 "user_hooks",
3312 ]
3313 .into_iter()
3314 .collect::<BTreeSet<_>>();
3315 if cfg!(feature = "web-fetch") {
3316 ids.insert("web_fetch");
3317 }
3318 ids
3319 }
3320
3321 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3323 let mut ids = expected_core_builtin_ids();
3324 ids.insert("agent_handoff");
3325 ids.insert("a2a_agent_delegation");
3326 ids
3327 }
3328
3329 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3330 registry.capabilities.keys().map(String::as_str).collect()
3331 }
3332
3333 #[test]
3343 fn test_capability_registry_with_builtins_dev() {
3344 let _lock = lock_env();
3346 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3347 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3348 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3349 assert!(registry.has("agent_handoff"));
3350 assert!(registry.has("a2a_agent_delegation"));
3351 }
3352
3353 #[test]
3354 fn test_capability_registry_with_builtins_prod() {
3355 let _lock = lock_env();
3357 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3358 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3359 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3360 assert!(!registry.has("docker_container"));
3362 assert!(!registry.has("agent_handoff"));
3363 assert!(!registry.has("a2a_agent_delegation"));
3364 }
3365
3366 #[test]
3367 fn test_capability_registry_runtime_builtins() {
3368 let _lock = lock_env();
3369 unsafe { std::env::remove_var("FEATURE_LUA") };
3370 let registry = CapabilityRegistry::runtime_builtins();
3371 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3372 assert!(registry.has("session_file_system"));
3373 #[cfg(feature = "web-fetch")]
3374 assert!(registry.has("web_fetch"));
3375 assert!(registry.has("bashkit_shell"));
3376
3377 for platform_only in [
3378 "platform_management",
3379 "model_scout",
3380 "openrouter_workspace",
3381 "openrouter_server_tools",
3382 "session_tasks",
3383 "session_schedule",
3384 "subagents",
3385 "background_execution",
3386 "session_sql_database",
3387 "knowledge_base",
3388 "knowledge_index",
3389 "sample_data",
3390 "data_knowledge",
3391 "fake_aws",
3392 "fake_crm",
3393 "fake_financial",
3394 "fake_warehouse",
3395 "test_math",
3396 "test_weather",
3397 "research",
3398 ] {
3399 assert!(
3400 !registry.has(platform_only),
3401 "`{platform_only}` should not be in the runtime default registry"
3402 );
3403 }
3404 }
3405
3406 #[test]
3407 fn test_agent_delegation_enabled_by_env_in_prod() {
3408 let _lock = lock_env();
3410 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3411 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3412 assert!(registry.has("agent_handoff"));
3413 assert!(registry.has("a2a_agent_delegation"));
3414 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3415 }
3416
3417 #[test]
3418 fn test_agent_delegation_disabled_by_env_in_dev() {
3419 let _lock = lock_env();
3421 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3422 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3423 assert!(!registry.has("agent_handoff"));
3424 assert!(!registry.has("a2a_agent_delegation"));
3425 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3426 }
3427
3428 #[test]
3429 fn test_capability_registry_get() {
3430 let registry = CapabilityRegistry::with_builtins();
3431
3432 let noop = registry.get("noop").unwrap();
3433 assert_eq!(noop.id(), "noop");
3434 assert_eq!(noop.name(), "No-Op");
3435 assert_eq!(noop.status(), CapabilityStatus::Available);
3436 }
3437
3438 #[test]
3446 fn builtin_capabilities_satisfy_registry_invariants() {
3447 let registry = CapabilityRegistry::with_builtins();
3448
3449 for cap in registry.list() {
3450 let id = cap.id();
3451 assert!(!id.is_empty(), "capability has an empty id");
3452 assert!(
3453 !cap.name().trim().is_empty(),
3454 "capability `{id}` has an empty name"
3455 );
3456
3457 assert!(
3460 registry.get(id).is_some(),
3461 "capability `{id}` does not resolve by its own id"
3462 );
3463
3464 for dep in cap.dependencies() {
3468 assert!(
3469 registry.get(dep).is_some(),
3470 "capability `{id}` depends on `{dep}`, which is not registered"
3471 );
3472 }
3473
3474 let mut seen = std::collections::HashSet::new();
3477 for tool in cap.tools() {
3478 let name = tool.name().to_string();
3479 assert!(
3480 !name.is_empty(),
3481 "capability `{id}` exposes a tool with an empty name"
3482 );
3483 assert!(
3484 seen.insert(name.clone()),
3485 "capability `{id}` exposes duplicate tool name `{name}`"
3486 );
3487 }
3488
3489 let mut def_seen = std::collections::HashSet::new();
3492 for def in cap.tool_definitions() {
3493 let name = def.name().to_string();
3494 assert!(
3495 !name.is_empty(),
3496 "capability `{id}` advertises a tool definition with an empty name"
3497 );
3498 assert!(
3499 def_seen.insert(name.clone()),
3500 "capability `{id}` advertises duplicate tool definition name `{name}`"
3501 );
3502 }
3503 }
3504 }
3505
3506 #[test]
3518 fn builtin_tools_have_narration_or_documented_generic_fallback() {
3519 use crate::tool_narration::{ToolNarrationContext, ToolNarrationPhase};
3520 use crate::tool_types::ToolCall;
3521
3522 const GENERIC_NARRATION_ALLOWLIST: &[(&str, &str)] = &[
3525 ("sample_data", "demo capability with fixture mounts"),
3527 (
3528 "data_knowledge",
3529 "demo knowledge scaffold; fixture data only",
3530 ),
3531 ("fake_aws", "demo/eval fixture tools"),
3532 ("fake_crm", "demo/eval fixture tools"),
3533 ("fake_financial", "demo/eval fixture tools"),
3534 ("fake_warehouse", "demo/eval fixture tools"),
3535 ("test_math", "test fixture capability"),
3536 ("test_weather", "test fixture capability"),
3537 (
3542 "platform_management",
3543 "operator admin surface; mutations narrate via narration_noun, reads use display names",
3544 ),
3545 (
3548 "model_scout",
3549 "operator model-routing tools; display-name presentation is adequate",
3550 ),
3551 (
3552 "openrouter_workspace",
3553 "operator OpenRouter inspection tools; display-name presentation is adequate",
3554 ),
3555 (
3558 "lua",
3559 "arbitrary sandboxed code execution; display-name presentation is adequate",
3560 ),
3561 ];
3562
3563 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3566 let ctx = ToolNarrationContext::default();
3567 let mut missing: Vec<String> = Vec::new();
3568
3569 for cap in registry.list() {
3570 let cap_id = cap.id().to_string();
3571 if GENERIC_NARRATION_ALLOWLIST
3572 .iter()
3573 .any(|(id, _)| *id == cap_id)
3574 {
3575 continue;
3576 }
3577
3578 for tool in cap.tools() {
3579 let def = tool.to_definition();
3580 if def.hints().narration_noun.is_some() {
3583 continue;
3584 }
3585
3586 let call = ToolCall {
3587 id: "call_narration_audit".to_string(),
3588 name: tool.name().to_string(),
3589 arguments: serde_json::json!({}),
3590 };
3591 if cap
3594 .narrate(Some(&def), &call, ToolNarrationPhase::Started, None, ctx)
3595 .is_none()
3596 {
3597 missing.push(format!("{cap_id}::{}", tool.name()));
3598 }
3599 }
3600 }
3601
3602 assert!(
3603 missing.is_empty(),
3604 "These built-in tools fall back to raw tool-call presentation. Implement \
3605 `Tool::narrate` (see specs/tool-narration.md), set a `narration_noun` hint, \
3606 or add a documented entry to GENERIC_NARRATION_ALLOWLIST: {missing:?}"
3607 );
3608 }
3609
3610 #[test]
3611 fn test_capability_registry_blueprint_with_capability() {
3612 struct BlueprintProviderCapability;
3613
3614 impl Capability for BlueprintProviderCapability {
3615 fn id(&self) -> &str {
3616 "blueprint_provider"
3617 }
3618 fn name(&self) -> &str {
3619 "Blueprint Provider"
3620 }
3621 fn description(&self) -> &str {
3622 "Capability that provides a blueprint for tests"
3623 }
3624 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3625 vec![AgentBlueprint {
3626 id: "test_blueprint",
3627 name: "Test Blueprint",
3628 description: "Blueprint for capability registry tests",
3629 model: BlueprintModel::Inherit,
3630 system_prompt: "Test prompt",
3631 tools: vec![],
3632 max_turns: None,
3633 config_schema: None,
3634 }]
3635 }
3636 }
3637
3638 let mut registry = CapabilityRegistry::new();
3639 registry.register(BlueprintProviderCapability);
3640
3641 let (capability_id, blueprint) = registry
3642 .blueprint_with_capability("test_blueprint")
3643 .expect("blueprint should resolve with capability id");
3644 assert_eq!(capability_id, "blueprint_provider");
3645 assert_eq!(blueprint.id, "test_blueprint");
3646 }
3647
3648 #[test]
3649 fn test_capability_registry_builder() {
3650 let registry = CapabilityRegistry::builder()
3651 .capability(NoopCapability)
3652 .capability(CurrentTimeCapability)
3653 .build();
3654
3655 assert!(registry.has("noop"));
3656 assert!(registry.has("current_time"));
3657 assert_eq!(registry.len(), 2);
3658 }
3659
3660 #[test]
3661 fn test_capability_status() {
3662 let registry = CapabilityRegistry::with_builtins();
3663
3664 let current_time = registry.get("current_time").unwrap();
3665 assert_eq!(current_time.status(), CapabilityStatus::Available);
3666
3667 let research = registry.get("research").unwrap();
3668 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3669 }
3670
3671 #[test]
3672 fn test_capability_icons_and_categories() {
3673 let registry = CapabilityRegistry::with_builtins();
3674
3675 let noop = registry.get("noop").unwrap();
3676 assert_eq!(noop.icon(), Some("circle-off"));
3677 assert_eq!(noop.category(), Some("Testing"));
3678
3679 let current_time = registry.get("current_time").unwrap();
3680 assert_eq!(current_time.icon(), Some("clock"));
3681 assert_eq!(current_time.category(), Some("Core"));
3682 }
3683
3684 #[test]
3685 fn test_system_prompt_preview_default_delegates_to_addition() {
3686 let registry = CapabilityRegistry::with_builtins();
3687
3688 let test_math = registry.get("test_math").unwrap();
3690 assert_eq!(
3691 test_math.system_prompt_preview().as_deref(),
3692 test_math.system_prompt_addition()
3693 );
3694
3695 let current_time = registry.get("current_time").unwrap();
3697 assert!(current_time.system_prompt_preview().is_none());
3698 assert!(current_time.system_prompt_addition().is_none());
3699 }
3700
3701 #[test]
3702 fn test_system_prompt_preview_dynamic_capability() {
3703 let registry = CapabilityRegistry::with_builtins();
3704 let cap = registry.get("agent_instructions").unwrap();
3705
3706 assert!(cap.system_prompt_addition().is_none());
3708 assert!(cap.system_prompt_preview().is_some());
3709 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3710 }
3711
3712 #[tokio::test]
3717 async fn test_apply_capabilities_empty() {
3718 let registry = CapabilityRegistry::with_builtins();
3719 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3720
3721 let applied =
3722 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3723
3724 assert_eq!(
3725 applied.runtime_agent.system_prompt,
3726 base_runtime_agent.system_prompt
3727 );
3728 assert!(applied.tool_registry.is_empty());
3729 assert!(applied.applied_ids.is_empty());
3730 }
3731
3732 #[tokio::test]
3733 async fn test_apply_capabilities_noop() {
3734 let registry = CapabilityRegistry::with_builtins();
3735 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3736
3737 let applied = apply_capabilities(
3738 base_runtime_agent.clone(),
3739 &["noop".to_string()],
3740 ®istry,
3741 &test_ctx(),
3742 )
3743 .await;
3744
3745 assert_eq!(
3747 applied.runtime_agent.system_prompt,
3748 base_runtime_agent.system_prompt
3749 );
3750 assert!(applied.tool_registry.is_empty());
3751 assert_eq!(applied.applied_ids, vec!["noop"]);
3752 }
3753
3754 #[tokio::test]
3755 async fn test_apply_capabilities_current_time() {
3756 let registry = CapabilityRegistry::with_builtins();
3757 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3758
3759 let applied = apply_capabilities(
3760 base_runtime_agent.clone(),
3761 &["current_time".to_string()],
3762 ®istry,
3763 &test_ctx(),
3764 )
3765 .await;
3766
3767 assert!(
3771 applied
3772 .runtime_agent
3773 .system_prompt
3774 .contains(FACTS_DYNAMIC_NOTE),
3775 "current_time should contribute the dynamic-facts note"
3776 );
3777 assert!(
3778 applied
3779 .runtime_agent
3780 .system_prompt
3781 .contains(&base_runtime_agent.system_prompt),
3782 "base prompt is preserved"
3783 );
3784 assert!(applied.tool_registry.has("get_current_time"));
3785 assert_eq!(applied.tool_registry.len(), 1);
3786 assert_eq!(applied.applied_ids, vec!["current_time"]);
3787 }
3788
3789 #[tokio::test]
3790 async fn test_apply_capabilities_skips_coming_soon() {
3791 let registry = CapabilityRegistry::with_builtins();
3792 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3793
3794 let applied = apply_capabilities(
3796 base_runtime_agent.clone(),
3797 &["research".to_string()],
3798 ®istry,
3799 &test_ctx(),
3800 )
3801 .await;
3802
3803 assert_eq!(
3805 applied.runtime_agent.system_prompt,
3806 base_runtime_agent.system_prompt
3807 );
3808 assert!(applied.applied_ids.is_empty()); }
3810
3811 #[tokio::test]
3812 async fn test_apply_capabilities_multiple() {
3813 let registry = CapabilityRegistry::with_builtins();
3814 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3815
3816 let applied = apply_capabilities(
3817 base_runtime_agent.clone(),
3818 &["noop".to_string(), "current_time".to_string()],
3819 ®istry,
3820 &test_ctx(),
3821 )
3822 .await;
3823
3824 assert!(applied.tool_registry.has("get_current_time"));
3825 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3826 }
3827
3828 #[tokio::test]
3829 async fn test_apply_capabilities_preserves_order() {
3830 let registry = CapabilityRegistry::with_builtins();
3831 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3832
3833 let applied = apply_capabilities(
3835 base_runtime_agent,
3836 &["current_time".to_string(), "noop".to_string()],
3837 ®istry,
3838 &test_ctx(),
3839 )
3840 .await;
3841
3842 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3843 }
3844
3845 #[tokio::test]
3846 async fn test_apply_capabilities_test_math() {
3847 let registry = CapabilityRegistry::with_builtins();
3848 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3849
3850 let applied = apply_capabilities(
3851 base_runtime_agent.clone(),
3852 &["test_math".to_string()],
3853 ®istry,
3854 &test_ctx(),
3855 )
3856 .await;
3857
3858 assert!(
3860 !applied
3861 .runtime_agent
3862 .system_prompt
3863 .contains("<capability id=\"test_math\">")
3864 );
3865 assert!(
3867 applied
3868 .runtime_agent
3869 .system_prompt
3870 .contains("You are a helpful assistant.")
3871 );
3872 assert!(applied.tool_registry.has("add"));
3873 assert!(applied.tool_registry.has("subtract"));
3874 assert!(applied.tool_registry.has("multiply"));
3875 assert!(applied.tool_registry.has("divide"));
3876 assert_eq!(applied.tool_registry.len(), 4);
3877 }
3878
3879 #[tokio::test]
3880 async fn test_apply_capabilities_test_weather() {
3881 let registry = CapabilityRegistry::with_builtins();
3882 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3883
3884 let applied = apply_capabilities(
3885 base_runtime_agent.clone(),
3886 &["test_weather".to_string()],
3887 ®istry,
3888 &test_ctx(),
3889 )
3890 .await;
3891
3892 assert!(
3894 !applied
3895 .runtime_agent
3896 .system_prompt
3897 .contains("<capability id=\"test_weather\">")
3898 );
3899 assert!(applied.tool_registry.has("get_weather"));
3900 assert!(applied.tool_registry.has("get_forecast"));
3901 assert_eq!(applied.tool_registry.len(), 2);
3902 }
3903
3904 #[tokio::test]
3905 async fn test_apply_capabilities_test_math_and_test_weather() {
3906 let registry = CapabilityRegistry::with_builtins();
3907 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3908
3909 let applied = apply_capabilities(
3910 base_runtime_agent.clone(),
3911 &["test_math".to_string(), "test_weather".to_string()],
3912 ®istry,
3913 &test_ctx(),
3914 )
3915 .await;
3916
3917 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3920 assert!(applied.tool_registry.has("get_weather"));
3921 }
3922
3923 #[tokio::test]
3924 async fn test_apply_capabilities_stateless_todo_list() {
3925 let registry = CapabilityRegistry::with_builtins();
3926 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3927
3928 let applied = apply_capabilities(
3929 base_runtime_agent.clone(),
3930 &["stateless_todo_list".to_string()],
3931 ®istry,
3932 &test_ctx(),
3933 )
3934 .await;
3935
3936 assert!(
3938 applied
3939 .runtime_agent
3940 .system_prompt
3941 .contains("Task Management")
3942 );
3943 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3944 assert!(applied.tool_registry.has("write_todos"));
3945 assert_eq!(applied.tool_registry.len(), 1);
3946 }
3947
3948 #[tokio::test]
3949 async fn test_apply_capabilities_web_fetch() {
3950 let registry = CapabilityRegistry::with_builtins();
3951 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3952
3953 let applied = apply_capabilities(
3954 base_runtime_agent.clone(),
3955 &["web_fetch".to_string()],
3956 ®istry,
3957 &test_ctx(),
3958 )
3959 .await;
3960
3961 assert!(
3963 applied
3964 .runtime_agent
3965 .system_prompt
3966 .contains(&base_runtime_agent.system_prompt)
3967 );
3968 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3969 assert!(applied.tool_registry.has("web_fetch"));
3970 assert_eq!(applied.tool_registry.len(), 1);
3971 }
3972
3973 #[tokio::test]
3978 async fn test_xml_tags_wrap_capability_prompts() {
3979 let registry = CapabilityRegistry::with_builtins();
3980 let collected =
3981 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3982 .await;
3983
3984 assert_eq!(collected.system_prompt_parts.len(), 1);
3985 let part = &collected.system_prompt_parts[0];
3986 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3987 assert!(part.ends_with("</capability>"));
3988 assert!(part.contains("Task Management"));
3989 }
3990
3991 #[tokio::test]
3992 async fn test_xml_tags_multiple_capabilities() {
3993 let registry = CapabilityRegistry::with_builtins();
3994 let collected = collect_capabilities(
3995 &[
3996 "stateless_todo_list".to_string(),
3997 "session_schedule".to_string(),
3998 ],
3999 ®istry,
4000 &test_ctx(),
4001 )
4002 .await;
4003
4004 assert_eq!(collected.system_prompt_parts.len(), 2);
4005 assert!(
4006 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
4007 );
4008 assert!(
4009 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
4010 );
4011
4012 let prefix = collected.system_prompt_prefix().unwrap();
4013 assert!(prefix.contains("</capability>\n\n<capability"));
4015 }
4016
4017 #[tokio::test]
4018 async fn test_xml_tags_system_prompt_wrapping() {
4019 let registry = CapabilityRegistry::with_builtins();
4020 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4021
4022 let applied = apply_capabilities(
4023 base,
4024 &["stateless_todo_list".to_string()],
4025 ®istry,
4026 &test_ctx(),
4027 )
4028 .await;
4029
4030 let prompt = &applied.runtime_agent.system_prompt;
4031 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4032 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
4034 assert!(prompt.contains("</capability>"));
4035 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4037 }
4038
4039 #[tokio::test]
4040 async fn test_no_xml_wrapping_without_capabilities() {
4041 let registry = CapabilityRegistry::with_builtins();
4042 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4043
4044 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
4045
4046 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4048 assert!(
4049 !applied
4050 .runtime_agent
4051 .system_prompt
4052 .contains("<system-prompt>")
4053 );
4054 }
4055
4056 #[tokio::test]
4057 async fn test_no_xml_wrapping_for_noop_capability() {
4058 let registry = CapabilityRegistry::with_builtins();
4059 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4060
4061 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
4063
4064 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4065 assert!(
4066 !applied
4067 .runtime_agent
4068 .system_prompt
4069 .contains("<system-prompt>")
4070 );
4071 }
4072
4073 #[tokio::test]
4078 async fn test_collect_capabilities_includes_mounts() {
4079 let registry = CapabilityRegistry::with_builtins();
4080
4081 let collected =
4082 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4083
4084 assert!(!collected.mounts.is_empty());
4085 assert_eq!(collected.mounts.len(), 1);
4086 assert_eq!(collected.mounts[0].path, "/samples");
4087 assert!(collected.mounts[0].is_readonly());
4088 }
4089
4090 #[tokio::test]
4091 async fn test_collect_capabilities_empty_mounts_by_default() {
4092 let registry = CapabilityRegistry::with_builtins();
4093
4094 let collected =
4096 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4097
4098 assert!(collected.mounts.is_empty());
4099 }
4100
4101 #[tokio::test]
4102 async fn test_dynamic_facts_add_note_without_static_block() {
4103 let registry = CapabilityRegistry::with_builtins();
4107 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4108 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4109 let prompt = collected.system_prompt_parts.join("\n");
4110 assert!(
4111 prompt.contains(FACTS_DYNAMIC_NOTE),
4112 "dynamic-facts note should be in the cached prompt"
4113 );
4114 assert!(
4115 !prompt.contains("<facts>\n"),
4116 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
4117 );
4118 }
4119
4120 #[tokio::test]
4121 async fn test_static_facts_fold_into_prompt() {
4122 struct StaticFactCap;
4123 impl Capability for StaticFactCap {
4124 fn id(&self) -> &str {
4125 "test_static_fact"
4126 }
4127 fn name(&self) -> &str {
4128 "Static Fact"
4129 }
4130 fn description(&self) -> &str {
4131 "test"
4132 }
4133 fn status(&self) -> CapabilityStatus {
4134 CapabilityStatus::Available
4135 }
4136 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
4137 vec![Fact::stat("workspace_root", "/workspace")]
4138 }
4139 }
4140 let mut registry = CapabilityRegistry::new();
4141 registry.register(StaticFactCap);
4142 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
4143 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4144 let prompt = collected.system_prompt_parts.join("\n");
4145 assert!(
4146 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
4147 "static fact should fold into the cached prompt; got: {prompt}"
4148 );
4149 assert!(
4150 !prompt.contains(FACTS_DYNAMIC_NOTE),
4151 "no dynamic note when only static facts exist"
4152 );
4153 }
4154
4155 #[test]
4156 fn test_collect_dynamic_facts_returns_current_time() {
4157 let registry = CapabilityRegistry::with_builtins();
4158 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4159 let facts = collect_dynamic_facts(
4160 &configs,
4161 ®istry,
4162 None,
4163 &FactsContext::new(SessionId::new()),
4164 );
4165 assert_eq!(facts.len(), 1);
4166 assert_eq!(facts[0].key, "current_time");
4167 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4168 }
4169
4170 #[tokio::test]
4171 async fn test_collect_capabilities_combines_mounts() {
4172 let registry = CapabilityRegistry::with_builtins();
4173
4174 let collected = collect_capabilities(
4177 &["sample_data".to_string(), "current_time".to_string()],
4178 ®istry,
4179 &test_ctx(),
4180 )
4181 .await;
4182
4183 assert_eq!(collected.mounts.len(), 1);
4184 assert!(
4186 collected
4187 .applied_ids
4188 .iter()
4189 .any(|id| id == "session_file_system")
4190 );
4191 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4192 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4193 }
4194
4195 #[test]
4196 fn test_sample_data_capability() {
4197 let registry = CapabilityRegistry::with_builtins();
4198 let cap = registry.get("sample_data").unwrap();
4199
4200 assert_eq!(cap.id(), "sample_data");
4201 assert_eq!(cap.name(), "Sample Data");
4202 assert_eq!(cap.status(), CapabilityStatus::Available);
4203
4204 assert!(cap.system_prompt_addition().is_some());
4206 assert!(cap.tools().is_empty());
4207
4208 assert!(!cap.mounts().is_empty());
4210 }
4211
4212 #[test]
4217 fn test_resolve_dependencies_empty() {
4218 let registry = CapabilityRegistry::with_builtins();
4219
4220 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4221
4222 assert!(resolved.resolved_ids.is_empty());
4223 assert!(resolved.added_as_dependencies.is_empty());
4224 assert!(resolved.user_selected.is_empty());
4225 }
4226
4227 #[test]
4228 fn test_resolve_dependencies_no_deps() {
4229 let registry = CapabilityRegistry::with_builtins();
4230
4231 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4233
4234 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4235 assert!(resolved.added_as_dependencies.is_empty());
4236 }
4237
4238 #[test]
4239 fn test_resolve_dependencies_with_deps() {
4240 let registry = CapabilityRegistry::with_builtins();
4241
4242 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4244
4245 assert_eq!(resolved.resolved_ids.len(), 2);
4247 let fs_pos = resolved
4248 .resolved_ids
4249 .iter()
4250 .position(|id| id == "session_file_system")
4251 .unwrap();
4252 let sd_pos = resolved
4253 .resolved_ids
4254 .iter()
4255 .position(|id| id == "sample_data")
4256 .unwrap();
4257 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4258
4259 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4261 }
4262
4263 #[test]
4264 fn test_resolve_dependencies_already_selected() {
4265 let registry = CapabilityRegistry::with_builtins();
4266
4267 let resolved = resolve_dependencies(
4269 &["session_file_system".to_string(), "sample_data".to_string()],
4270 ®istry,
4271 )
4272 .unwrap();
4273
4274 assert_eq!(resolved.resolved_ids.len(), 2);
4275 assert!(resolved.added_as_dependencies.is_empty());
4277 }
4278
4279 #[test]
4280 fn test_resolve_dependencies_preserves_order() {
4281 let registry = CapabilityRegistry::with_builtins();
4282
4283 let resolved =
4285 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4286 .unwrap();
4287
4288 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4289 }
4290
4291 #[test]
4292 fn test_resolve_dependencies_unknown_capability() {
4293 let registry = CapabilityRegistry::with_builtins();
4294
4295 let resolved =
4297 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4298
4299 assert!(resolved.resolved_ids.is_empty());
4300 }
4301
4302 #[test]
4303 fn test_get_dependencies() {
4304 let registry = CapabilityRegistry::with_builtins();
4305
4306 let deps = get_dependencies("sample_data", ®istry);
4308 assert_eq!(deps, vec!["session_file_system"]);
4309
4310 let deps = get_dependencies("current_time", ®istry);
4312 assert!(deps.is_empty());
4313
4314 let deps = get_dependencies("unknown", ®istry);
4316 assert!(deps.is_empty());
4317 }
4318
4319 #[test]
4320 fn test_sample_data_has_dependency() {
4321 let registry = CapabilityRegistry::with_builtins();
4322 let cap = registry.get("sample_data").unwrap();
4323
4324 let deps = cap.dependencies();
4325 assert_eq!(deps.len(), 1);
4326 assert_eq!(deps[0], "session_file_system");
4327 }
4328
4329 #[test]
4330 fn test_noop_has_no_dependencies() {
4331 let registry = CapabilityRegistry::with_builtins();
4332 let cap = registry.get("noop").unwrap();
4333
4334 assert!(cap.dependencies().is_empty());
4335 }
4336
4337 #[test]
4341 fn test_circular_dependency_error() {
4342 struct CapA;
4344 struct CapB;
4345
4346 impl Capability for CapA {
4347 fn id(&self) -> &str {
4348 "test_cap_a"
4349 }
4350 fn name(&self) -> &str {
4351 "Test A"
4352 }
4353 fn description(&self) -> &str {
4354 "Test capability A"
4355 }
4356 fn dependencies(&self) -> Vec<&'static str> {
4357 vec!["test_cap_b"]
4358 }
4359 }
4360
4361 impl Capability for CapB {
4362 fn id(&self) -> &str {
4363 "test_cap_b"
4364 }
4365 fn name(&self) -> &str {
4366 "Test B"
4367 }
4368 fn description(&self) -> &str {
4369 "Test capability B"
4370 }
4371 fn dependencies(&self) -> Vec<&'static str> {
4372 vec!["test_cap_a"]
4373 }
4374 }
4375
4376 let mut registry = CapabilityRegistry::new();
4377 registry.register(CapA);
4378 registry.register(CapB);
4379
4380 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4381
4382 assert!(result.is_err());
4383 match result.unwrap_err() {
4384 DependencyError::CircularDependency { capability_id, .. } => {
4385 assert_eq!(capability_id, "test_cap_a");
4386 }
4387 _ => panic!("Expected CircularDependency error"),
4388 }
4389 }
4390
4391 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4396
4397 struct FilterTestCapability {
4399 priority: i32,
4400 }
4401
4402 impl Capability for FilterTestCapability {
4403 fn id(&self) -> &str {
4404 "filter_test"
4405 }
4406 fn name(&self) -> &str {
4407 "Filter Test"
4408 }
4409 fn description(&self) -> &str {
4410 "Test capability with message filter"
4411 }
4412 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4413 Some(Arc::new(FilterTestProvider {
4414 priority: self.priority,
4415 }))
4416 }
4417 }
4418
4419 struct FilterTestProvider {
4420 priority: i32,
4421 }
4422
4423 impl MessageFilterProvider for FilterTestProvider {
4424 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4425 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4427 query
4428 .filters
4429 .push(MessageFilter::Search(search.to_string()));
4430 }
4431 }
4432
4433 fn priority(&self) -> i32 {
4434 self.priority
4435 }
4436 }
4437
4438 #[tokio::test]
4439 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4440 let registry = CapabilityRegistry::with_builtins();
4441 let configs = vec![AgentCapabilityConfig {
4442 capability_ref: CapabilityId::new("current_time"),
4443 config: serde_json::json!({}),
4444 }];
4445
4446 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4447
4448 assert!(collected.message_filter_providers.is_empty());
4449 assert!(!collected.has_message_filters());
4450 }
4451
4452 #[tokio::test]
4453 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4454 let mut registry = CapabilityRegistry::new();
4455 registry.register(FilterTestCapability { priority: 0 });
4456
4457 let configs = vec![AgentCapabilityConfig {
4458 capability_ref: CapabilityId::new("filter_test"),
4459 config: serde_json::json!({ "search": "hello" }),
4460 }];
4461
4462 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4463
4464 assert_eq!(collected.message_filter_providers.len(), 1);
4465 assert!(collected.has_message_filters());
4466 }
4467
4468 #[tokio::test]
4469 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4470 struct HighPriorityCapability;
4472 struct LowPriorityCapability;
4473
4474 impl Capability for HighPriorityCapability {
4475 fn id(&self) -> &str {
4476 "high_priority"
4477 }
4478 fn name(&self) -> &str {
4479 "High Priority"
4480 }
4481 fn description(&self) -> &str {
4482 "Test"
4483 }
4484 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4485 Some(Arc::new(FilterTestProvider { priority: 10 }))
4486 }
4487 }
4488
4489 impl Capability for LowPriorityCapability {
4490 fn id(&self) -> &str {
4491 "low_priority"
4492 }
4493 fn name(&self) -> &str {
4494 "Low Priority"
4495 }
4496 fn description(&self) -> &str {
4497 "Test"
4498 }
4499 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4500 Some(Arc::new(FilterTestProvider { priority: -5 }))
4501 }
4502 }
4503
4504 let mut registry = CapabilityRegistry::new();
4505 registry.register(HighPriorityCapability);
4506 registry.register(LowPriorityCapability);
4507
4508 let configs = vec![
4510 AgentCapabilityConfig {
4511 capability_ref: CapabilityId::new("high_priority"),
4512 config: serde_json::json!({}),
4513 },
4514 AgentCapabilityConfig {
4515 capability_ref: CapabilityId::new("low_priority"),
4516 config: serde_json::json!({}),
4517 },
4518 ];
4519
4520 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4521
4522 assert_eq!(collected.message_filter_providers.len(), 2);
4524 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4525 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4526 }
4527
4528 #[tokio::test]
4529 async fn test_collected_capabilities_apply_message_filters() {
4530 let mut registry = CapabilityRegistry::new();
4531 registry.register(FilterTestCapability { priority: 0 });
4532
4533 let configs = vec![AgentCapabilityConfig {
4534 capability_ref: CapabilityId::new("filter_test"),
4535 config: serde_json::json!({ "search": "test_query" }),
4536 }];
4537
4538 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4539
4540 let session_id: SessionId = Uuid::now_v7().into();
4542 let mut query = MessageQuery::new(session_id);
4543
4544 collected.apply_message_filters(&mut query);
4545
4546 assert_eq!(query.filters.len(), 1);
4548 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4549 }
4550
4551 #[tokio::test]
4552 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4553 struct SearchCapability {
4554 id: &'static str,
4555 search_term: &'static str,
4556 priority: i32,
4557 }
4558
4559 struct SearchProvider {
4560 search_term: &'static str,
4561 priority: i32,
4562 }
4563
4564 impl MessageFilterProvider for SearchProvider {
4565 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4566 query
4567 .filters
4568 .push(MessageFilter::Search(self.search_term.to_string()));
4569 }
4570
4571 fn priority(&self) -> i32 {
4572 self.priority
4573 }
4574 }
4575
4576 impl Capability for SearchCapability {
4577 fn id(&self) -> &str {
4578 self.id
4579 }
4580 fn name(&self) -> &str {
4581 "Search"
4582 }
4583 fn description(&self) -> &str {
4584 "Test"
4585 }
4586 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4587 Some(Arc::new(SearchProvider {
4588 search_term: self.search_term,
4589 priority: self.priority,
4590 }))
4591 }
4592 }
4593
4594 let mut registry = CapabilityRegistry::new();
4595 registry.register(SearchCapability {
4596 id: "cap_a",
4597 search_term: "alpha",
4598 priority: 5,
4599 });
4600 registry.register(SearchCapability {
4601 id: "cap_b",
4602 search_term: "beta",
4603 priority: 1,
4604 });
4605 registry.register(SearchCapability {
4606 id: "cap_c",
4607 search_term: "gamma",
4608 priority: 10,
4609 });
4610
4611 let configs = vec![
4612 AgentCapabilityConfig {
4613 capability_ref: CapabilityId::new("cap_a"),
4614 config: serde_json::json!({}),
4615 },
4616 AgentCapabilityConfig {
4617 capability_ref: CapabilityId::new("cap_b"),
4618 config: serde_json::json!({}),
4619 },
4620 AgentCapabilityConfig {
4621 capability_ref: CapabilityId::new("cap_c"),
4622 config: serde_json::json!({}),
4623 },
4624 ];
4625
4626 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4627
4628 let session_id: SessionId = Uuid::now_v7().into();
4629 let mut query = MessageQuery::new(session_id);
4630
4631 collected.apply_message_filters(&mut query);
4632
4633 assert_eq!(query.filters.len(), 3);
4635 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4636 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4637 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4638 }
4639
4640 #[test]
4641 fn test_capability_without_message_filter_returns_none() {
4642 let registry = CapabilityRegistry::with_builtins();
4643
4644 let noop = registry.get("noop").unwrap();
4645 assert!(noop.message_filter_provider().is_none());
4646
4647 let current_time = registry.get("current_time").unwrap();
4648 assert!(current_time.message_filter_provider().is_none());
4649 }
4650
4651 #[tokio::test]
4652 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4653 let mut registry = CapabilityRegistry::new();
4654 registry.register(FilterTestCapability { priority: 0 });
4655
4656 let test_config = serde_json::json!({
4657 "search": "custom_search",
4658 "extra_field": 42
4659 });
4660
4661 let configs = vec![AgentCapabilityConfig {
4662 capability_ref: CapabilityId::new("filter_test"),
4663 config: test_config.clone(),
4664 }];
4665
4666 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4667
4668 assert_eq!(collected.message_filter_providers.len(), 1);
4670 let (_, stored_config) = &collected.message_filter_providers[0];
4671 assert_eq!(*stored_config, test_config);
4672 }
4673
4674 #[test]
4679 fn test_collect_message_filters_only_collects_filters() {
4680 let mut registry = CapabilityRegistry::new();
4681 registry.register(FilterTestCapability { priority: 0 });
4682
4683 let configs = vec![AgentCapabilityConfig {
4684 capability_ref: CapabilityId::new("filter_test"),
4685 config: serde_json::json!({ "search": "test_query" }),
4686 }];
4687
4688 let collected = collect_message_filters_only(&configs, ®istry);
4689
4690 let session_id: SessionId = Uuid::now_v7().into();
4691 let mut query = MessageQuery::new(session_id);
4692 collected.apply_message_filters(&mut query);
4693
4694 assert_eq!(query.filters.len(), 1);
4695 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4696 }
4697
4698 #[test]
4699 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4700 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4701
4702 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4704 assert_eq!(with["compaction_active"], serde_json::json!(true));
4705 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4706
4707 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4708 assert!(without.get("compaction_active").is_none());
4709
4710 let other = message_filter_config_for("other", &base, true);
4712 assert!(other.get("compaction_active").is_none());
4713
4714 let null_base = message_filter_config_for(
4716 INFINITY_CONTEXT_CAPABILITY_ID,
4717 &serde_json::Value::Null,
4718 true,
4719 );
4720 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4721 }
4722
4723 #[test]
4724 fn test_infinity_context_defers_to_compaction_end_to_end() {
4725 use crate::message::Message;
4726
4727 let mut registry = CapabilityRegistry::new();
4728 registry.register(InfinityContextCapability);
4729 registry.register(CompactionCapability);
4730
4731 let tight = serde_json::json!({
4732 "context_budget_tokens": 1,
4733 "min_recent_messages": 1
4734 });
4735
4736 let solo = vec![AgentCapabilityConfig {
4738 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4739 config: tight.clone(),
4740 }];
4741 let mut messages = vec![
4742 Message::user("task"),
4743 Message::assistant("old ".repeat(400)),
4744 Message::user("recent"),
4745 ];
4746 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4747 assert!(
4748 messages
4749 .iter()
4750 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4751 "infinity context alone should trim and notice"
4752 );
4753
4754 let both = vec![
4756 AgentCapabilityConfig {
4757 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4758 config: tight,
4759 },
4760 AgentCapabilityConfig {
4761 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4762 config: serde_json::json!({}),
4763 },
4764 ];
4765 let mut messages = vec![
4766 Message::user("task"),
4767 Message::assistant("old ".repeat(400)),
4768 Message::user("recent"),
4769 ];
4770 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4771 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4772 assert!(
4773 messages
4774 .iter()
4775 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4776 "no hidden-history notice when compaction is the active reducer"
4777 );
4778 }
4779
4780 #[test]
4781 fn test_compaction_is_enabled_detects_compaction() {
4782 let mut registry = CapabilityRegistry::new();
4783 registry.register(CompactionCapability);
4784
4785 let with_compaction = vec![AgentCapabilityConfig {
4786 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4787 config: serde_json::json!({}),
4788 }];
4789 assert!(compaction_is_enabled(&with_compaction, ®istry));
4790
4791 let without = vec![AgentCapabilityConfig {
4792 capability_ref: CapabilityId::new("current_time"),
4793 config: serde_json::json!({}),
4794 }];
4795 assert!(!compaction_is_enabled(&without, ®istry));
4796 }
4797
4798 #[test]
4799 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4800 let registry = CapabilityRegistry::new();
4801
4802 let configs = vec![AgentCapabilityConfig {
4803 capability_ref: CapabilityId::new("nonexistent"),
4804 config: serde_json::json!({}),
4805 }];
4806
4807 let collected = collect_message_filters_only(&configs, ®istry);
4808 assert!(collected.message_filter_providers.is_empty());
4809 }
4810
4811 #[test]
4812 fn test_collect_message_filters_only_preserves_priority_order() {
4813 struct PriorityFilterCap {
4814 id: &'static str,
4815 search_term: &'static str,
4816 priority: i32,
4817 }
4818
4819 struct PriorityFilterProvider {
4820 search_term: &'static str,
4821 priority: i32,
4822 }
4823
4824 impl Capability for PriorityFilterCap {
4825 fn id(&self) -> &str {
4826 self.id
4827 }
4828 fn name(&self) -> &str {
4829 self.id
4830 }
4831 fn description(&self) -> &str {
4832 "priority test"
4833 }
4834 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4835 Some(Arc::new(PriorityFilterProvider {
4836 search_term: self.search_term,
4837 priority: self.priority,
4838 }))
4839 }
4840 }
4841
4842 impl MessageFilterProvider for PriorityFilterProvider {
4843 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4844 query
4845 .filters
4846 .push(MessageFilter::Search(self.search_term.to_string()));
4847 }
4848 fn priority(&self) -> i32 {
4849 self.priority
4850 }
4851 }
4852
4853 let mut registry = CapabilityRegistry::new();
4854 registry.register(PriorityFilterCap {
4855 id: "gamma",
4856 search_term: "gamma",
4857 priority: 10,
4858 });
4859 registry.register(PriorityFilterCap {
4860 id: "alpha",
4861 search_term: "alpha",
4862 priority: 5,
4863 });
4864 registry.register(PriorityFilterCap {
4865 id: "beta",
4866 search_term: "beta",
4867 priority: 1,
4868 });
4869
4870 let configs = vec![
4871 AgentCapabilityConfig {
4872 capability_ref: CapabilityId::new("gamma"),
4873 config: serde_json::json!({}),
4874 },
4875 AgentCapabilityConfig {
4876 capability_ref: CapabilityId::new("alpha"),
4877 config: serde_json::json!({}),
4878 },
4879 AgentCapabilityConfig {
4880 capability_ref: CapabilityId::new("beta"),
4881 config: serde_json::json!({}),
4882 },
4883 ];
4884
4885 let collected = collect_message_filters_only(&configs, ®istry);
4886
4887 let session_id: SessionId = Uuid::now_v7().into();
4888 let mut query = MessageQuery::new(session_id);
4889 collected.apply_message_filters(&mut query);
4890
4891 assert_eq!(query.filters.len(), 3);
4893 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4894 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4895 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4896 }
4897
4898 #[test]
4899 fn test_collect_message_filters_only_post_load_invoked() {
4900 use crate::message::Message;
4901
4902 struct PostLoadCap;
4903 struct PostLoadProvider;
4904
4905 impl Capability for PostLoadCap {
4906 fn id(&self) -> &str {
4907 "post_load_test"
4908 }
4909 fn name(&self) -> &str {
4910 "PostLoad Test"
4911 }
4912 fn description(&self) -> &str {
4913 "test"
4914 }
4915 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4916 Some(Arc::new(PostLoadProvider))
4917 }
4918 }
4919
4920 impl MessageFilterProvider for PostLoadProvider {
4921 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4922 fn priority(&self) -> i32 {
4923 0
4924 }
4925 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4926 messages.reverse();
4928 }
4929 }
4930
4931 let mut registry = CapabilityRegistry::new();
4932 registry.register(PostLoadCap);
4933
4934 let configs = vec![AgentCapabilityConfig {
4935 capability_ref: CapabilityId::new("post_load_test"),
4936 config: serde_json::json!({}),
4937 }];
4938
4939 let collected = collect_message_filters_only(&configs, ®istry);
4940
4941 let mut messages = vec![Message::user("first"), Message::user("second")];
4942 collected.apply_post_load_filters(&mut messages);
4943
4944 assert_eq!(messages[0].text(), Some("second"));
4946 assert_eq!(messages[1].text(), Some("first"));
4947 }
4948
4949 #[test]
4950 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4951 use crate::tool_types::ToolCall;
4952
4953 fn tool_heavy_messages() -> Vec<Message> {
4954 let mut messages = vec![Message::user("inspect files repeatedly")];
4955 for index in 0..9 {
4956 let call_id = format!("call_{index}");
4957 messages.push(Message::assistant_with_tools(
4958 "",
4959 vec![ToolCall {
4960 id: call_id.clone(),
4961 name: "read_file".to_string(),
4962 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4963 }],
4964 ));
4965 messages.push(Message::tool_result(
4966 call_id,
4967 Some(serde_json::json!({
4968 "path": "/workspace/src/lib.rs",
4969 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4970 "total_lines": 1000,
4971 "lines_shown": {"start": 1, "end": 1000},
4972 "truncated": false
4973 })),
4974 None,
4975 ));
4976 }
4977 messages
4978 }
4979
4980 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4981 messages[2]
4982 .tool_result_content()
4983 .and_then(|result| result.result.as_ref())
4984 .and_then(|result| result.get("masked"))
4985 .and_then(|masked| masked.as_bool())
4986 .unwrap_or(false)
4987 }
4988
4989 let mut registry = CapabilityRegistry::new();
4990 registry.register(CompactionCapability);
4991 let context = ModelViewContext {
4992 session_id: SessionId::new(),
4993 prior_usage: None,
4994 };
4995
4996 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4997 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4998 assert!(!first_tool_result_is_masked(&unmasked));
4999
5000 let compaction = collect_model_view_providers(
5001 &[AgentCapabilityConfig {
5002 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
5003 config: serde_json::json!({}),
5004 }],
5005 ®istry,
5006 None,
5007 );
5008 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
5009 assert!(first_tool_result_is_masked(&masked));
5010 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
5011 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
5012 }
5013
5014 struct DelegatingFilterCap {
5017 id: &'static str,
5018 inner: std::sync::Arc<InnerFilterCap>,
5019 }
5020 struct InnerFilterCap;
5021
5022 impl Capability for InnerFilterCap {
5023 fn id(&self) -> &str {
5024 "inner_filter"
5025 }
5026 fn name(&self) -> &str {
5027 "Inner Filter"
5028 }
5029 fn description(&self) -> &str {
5030 "inner"
5031 }
5032 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5033 Some(std::sync::Arc::new(SentinelFilter))
5034 }
5035 }
5036 struct SentinelFilter;
5037 impl MessageFilterProvider for SentinelFilter {
5038 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
5039 }
5040 impl Capability for DelegatingFilterCap {
5041 fn id(&self) -> &str {
5042 self.id
5043 }
5044 fn name(&self) -> &str {
5045 "Delegating Filter"
5046 }
5047 fn description(&self) -> &str {
5048 "delegating"
5049 }
5050 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5051 None }
5053 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5054 Some(&*self.inner)
5055 }
5056 }
5057
5058 #[test]
5059 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
5060 let inner = std::sync::Arc::new(InnerFilterCap);
5061 let outer = DelegatingFilterCap {
5062 id: "delegating_filter",
5063 inner: inner.clone(),
5064 };
5065
5066 let mut registry = CapabilityRegistry::new();
5067 registry.register(outer);
5068
5069 let configs = vec![AgentCapabilityConfig {
5070 capability_ref: CapabilityId::new("delegating_filter"),
5071 config: serde_json::json!({}),
5072 }];
5073
5074 let collected = collect_message_filters_only(&configs, ®istry);
5077 assert_eq!(
5078 collected.message_filter_providers.len(),
5079 1,
5080 "provider from resolved inner capability must be collected"
5081 );
5082 }
5083
5084 struct DelegatingMvpCap {
5085 id: &'static str,
5086 inner: std::sync::Arc<InnerMvpCap>,
5087 }
5088 struct InnerMvpCap;
5089
5090 impl Capability for InnerMvpCap {
5091 fn id(&self) -> &str {
5092 "inner_mvp"
5093 }
5094 fn name(&self) -> &str {
5095 "Inner MVP"
5096 }
5097 fn description(&self) -> &str {
5098 "inner"
5099 }
5100 fn model_view_provider(
5101 &self,
5102 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5103 struct NoopMvp;
5105 impl crate::capabilities::ModelViewProvider for NoopMvp {
5106 fn apply_model_view(
5107 &self,
5108 messages: Vec<Message>,
5109 _config: &serde_json::Value,
5110 _context: &ModelViewContext<'_>,
5111 ) -> Vec<Message> {
5112 messages
5113 }
5114 }
5115 Some(std::sync::Arc::new(NoopMvp))
5116 }
5117 }
5118 impl Capability for DelegatingMvpCap {
5119 fn id(&self) -> &str {
5120 self.id
5121 }
5122 fn name(&self) -> &str {
5123 "Delegating MVP"
5124 }
5125 fn description(&self) -> &str {
5126 "delegating"
5127 }
5128 fn model_view_provider(
5129 &self,
5130 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5131 None }
5133 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5134 Some(&*self.inner)
5135 }
5136 }
5137
5138 #[test]
5139 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
5140 let inner = std::sync::Arc::new(InnerMvpCap);
5141 let outer = DelegatingMvpCap {
5142 id: "delegating_mvp",
5143 inner: inner.clone(),
5144 };
5145
5146 let mut registry = CapabilityRegistry::new();
5147 registry.register(outer);
5148
5149 let configs = vec![AgentCapabilityConfig {
5150 capability_ref: CapabilityId::new("delegating_mvp"),
5151 config: serde_json::json!({}),
5152 }];
5153
5154 let collected = collect_model_view_providers(&configs, ®istry, None);
5157 assert_eq!(
5158 collected.model_view_providers.len(),
5159 1,
5160 "provider from resolved inner capability must be collected"
5161 );
5162 }
5163
5164 #[tokio::test]
5174 async fn test_bashkit_shell_capability_produces_bash_tool() {
5175 let registry = CapabilityRegistry::with_builtins();
5176 let collected =
5177 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5178
5179 let tool_names: Vec<&str> = collected
5180 .tool_definitions
5181 .iter()
5182 .map(|t| t.name())
5183 .collect();
5184 assert!(
5185 tool_names.contains(&"bash"),
5186 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5187 tool_names
5188 );
5189 assert!(
5190 !collected.tools.is_empty(),
5191 "bashkit_shell must provide tool implementations"
5192 );
5193 }
5194
5195 #[tokio::test]
5196 async fn test_generic_harness_capability_set_produces_bash_tool() {
5197 let generic_harness_caps = vec![
5200 "session_file_system".to_string(),
5201 "bashkit_shell".to_string(),
5202 "web_fetch".to_string(),
5203 "session_storage".to_string(),
5204 "session".to_string(),
5205 "agent_instructions".to_string(),
5206 "skills".to_string(),
5207 "infinity_context".to_string(),
5208 "auto_tool_search".to_string(),
5209 ];
5210
5211 let registry = CapabilityRegistry::with_builtins();
5212 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5213
5214 let tool_names: Vec<&str> = collected
5215 .tool_definitions
5216 .iter()
5217 .map(|t| t.name())
5218 .collect();
5219 assert!(
5220 tool_names.contains(&"bash"),
5221 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5222 tool_names
5223 );
5224 }
5225
5226 #[tokio::test]
5227 async fn test_collect_capabilities_tool_count_matches_definitions() {
5228 let registry = CapabilityRegistry::with_builtins();
5231 let collected =
5232 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5233
5234 assert_eq!(
5235 collected.tools.len(),
5236 collected.tool_definitions.len(),
5237 "tool implementations ({}) must match tool definitions ({})",
5238 collected.tools.len(),
5239 collected.tool_definitions.len(),
5240 );
5241 }
5242
5243 #[tokio::test]
5247 async fn test_collect_capabilities_resolves_dependencies() {
5248 let registry = CapabilityRegistry::with_builtins();
5251 let collected =
5252 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5253
5254 assert!(
5256 collected
5257 .applied_ids
5258 .iter()
5259 .any(|id| id == "session_file_system"),
5260 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5261 collected.applied_ids
5262 );
5263
5264 let tool_names: Vec<&str> = collected
5265 .tool_definitions
5266 .iter()
5267 .map(|t| t.name())
5268 .collect();
5269
5270 assert!(
5272 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5273 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5274 tool_names
5275 );
5276
5277 assert_eq!(
5279 collected.tools.len(),
5280 collected.tool_definitions.len(),
5281 "dependency-added tools must have implementations, not just definitions"
5282 );
5283 }
5284
5285 #[test]
5286 fn test_defaults_do_not_include_bash() {
5287 let registry = crate::ToolRegistry::with_defaults();
5290 assert!(
5291 !registry.has("bash"),
5292 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5293 );
5294 }
5295
5296 #[tokio::test]
5303 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5304 let registry = CapabilityRegistry::with_builtins();
5305 let collected =
5306 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5307
5308 let tool_names: Vec<&str> = collected
5309 .tool_definitions
5310 .iter()
5311 .map(|t| t.name())
5312 .collect();
5313 assert!(
5314 tool_names.contains(&"spawn_background"),
5315 "spawn_background must be auto-activated when bashkit_shell (a \
5316 background-capable tool) is in the agent's capability set; got: {:?}",
5317 tool_names
5318 );
5319 assert!(
5320 collected
5321 .applied_ids
5322 .iter()
5323 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5324 "background_execution must be in applied_ids when auto-activated; \
5325 got: {:?}",
5326 collected.applied_ids
5327 );
5328
5329 assert!(
5331 collected
5332 .tools
5333 .iter()
5334 .any(|t| t.name() == "spawn_background"),
5335 "spawn_background tool implementation must be present alongside the \
5336 definition (lockstep contract)"
5337 );
5338 }
5339
5340 #[tokio::test]
5343 async fn test_background_execution_does_not_auto_activate_without_hint() {
5344 let registry = CapabilityRegistry::with_builtins();
5345 let collected =
5347 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5348
5349 let tool_names: Vec<&str> = collected
5350 .tool_definitions
5351 .iter()
5352 .map(|t| t.name())
5353 .collect();
5354 assert!(
5355 !tool_names.contains(&"spawn_background"),
5356 "spawn_background must NOT be activated without a background-capable \
5357 tool; got: {:?}",
5358 tool_names
5359 );
5360 assert!(
5361 !collected
5362 .applied_ids
5363 .iter()
5364 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5365 "background_execution must not appear in applied_ids when no \
5366 background-capable tool is present; got: {:?}",
5367 collected.applied_ids
5368 );
5369 }
5370
5371 #[tokio::test]
5372 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5373 let registry = CapabilityRegistry::with_builtins();
5374 let collected = collect_capabilities(
5375 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5376 ®istry,
5377 &test_ctx(),
5378 )
5379 .await;
5380
5381 assert!(
5382 collected
5383 .tools
5384 .iter()
5385 .any(|tool| tool.name() == "spawn_agent"),
5386 "subagent-only sessions should get the unified spawn_agent adapter"
5387 );
5388 let spawn_agent = collected
5389 .tool_definitions
5390 .iter()
5391 .find(|tool| tool.name() == "spawn_agent")
5392 .expect("spawn_agent definition");
5393 assert_eq!(
5394 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5395 serde_json::json!(["subagent"])
5396 );
5397 assert_eq!(
5398 spawn_agent.concurrency_class(),
5399 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5400 "unified spawn_agent must serialize same-batch spawns before cap checks"
5401 );
5402 }
5403
5404 #[tokio::test]
5405 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5406 let mut registry = CapabilityRegistry::new();
5407 registry.register(AgentHandoffCapability);
5408 let agent_id = crate::typed_id::AgentId::new();
5409 let harness_id = crate::typed_id::HarnessId::new();
5410 let configs = vec![AgentCapabilityConfig {
5411 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5412 config: serde_json::json!({
5413 "targets": [{
5414 "id": "aws_operator",
5415 "name": "AWS Operator",
5416 "agent_id": agent_id,
5417 "harness_id": harness_id
5418 }]
5419 }),
5420 }];
5421 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5422
5423 assert!(
5424 collected
5425 .tools
5426 .iter()
5427 .any(|tool| tool.name() == "spawn_agent"),
5428 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5429 );
5430 let spawn_agent = collected
5431 .tool_definitions
5432 .iter()
5433 .find(|tool| tool.name() == "spawn_agent")
5434 .expect("spawn_agent definition");
5435 assert_eq!(
5436 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5437 serde_json::json!(["agent"])
5438 );
5439 }
5440
5441 #[tokio::test]
5442 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5443 let mut registry = CapabilityRegistry::new();
5444 registry.register(SubagentCapability);
5445 registry.register(AgentHandoffCapability);
5446
5447 let agent_id = crate::typed_id::AgentId::new();
5448 let harness_id = crate::typed_id::HarnessId::new();
5449 let configs = vec![
5450 AgentCapabilityConfig {
5451 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5452 config: serde_json::json!({}),
5453 },
5454 AgentCapabilityConfig {
5455 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5456 config: serde_json::json!({
5457 "targets": [{
5458 "id": "aws_operator",
5459 "name": "AWS Operator",
5460 "agent_id": agent_id,
5461 "harness_id": harness_id
5462 }]
5463 }),
5464 },
5465 ];
5466
5467 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5468 let spawn_agent_defs: Vec<_> = collected
5469 .tool_definitions
5470 .iter()
5471 .filter(|tool| tool.name() == "spawn_agent")
5472 .collect();
5473
5474 assert_eq!(spawn_agent_defs.len(), 1);
5475 let schema = spawn_agent_defs[0].parameters();
5476 assert_eq!(
5477 schema["properties"]["target"]["properties"]["type"]["enum"],
5478 serde_json::json!(["subagent", "agent"])
5479 );
5480 assert!(schema.get("oneOf").is_none());
5483 assert!(schema.get("anyOf").is_none());
5484 assert!(schema.get("allOf").is_none());
5485 assert_eq!(
5486 schema["required"],
5487 serde_json::json!(["name", "instructions", "target"])
5488 );
5489 assert_eq!(
5490 schema["properties"]["target"]["oneOf"],
5491 serde_json::json!([
5492 {
5493 "properties": {"type": {"const": "subagent"}}
5494 },
5495 {
5496 "properties": {"type": {"const": "agent"}},
5497 "required": ["type", "id"]
5498 }
5499 ])
5500 );
5501 }
5502
5503 #[cfg(feature = "a2a")]
5504 #[tokio::test]
5505 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5506 let mut registry = CapabilityRegistry::new();
5507 registry.register(SubagentCapability);
5508 registry.register(A2aAgentDelegationCapability);
5509
5510 let configs = vec![
5511 AgentCapabilityConfig {
5512 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5513 config: serde_json::json!({}),
5514 },
5515 AgentCapabilityConfig {
5516 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5517 config: serde_json::json!({
5518 "agents": [{
5519 "id": "local_app",
5520 "name": "Local App",
5521 "base_url": "https://example.com"
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 assert_eq!(
5536 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5537 serde_json::json!(["subagent", "external_a2a"])
5538 );
5539 assert_eq!(
5540 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5541 serde_json::json!(["background", "foreground"])
5542 );
5543 assert!(
5544 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5545 .as_str()
5546 .expect("mode description")
5547 .contains("wait")
5548 );
5549 let schema = spawn_agent_defs[0].parameters();
5550 assert!(schema.get("oneOf").is_none());
5551 assert_eq!(
5555 schema["required"],
5556 serde_json::json!(["name", "instructions", "target"])
5557 );
5558 assert_eq!(
5559 schema["properties"]["target"]["oneOf"],
5560 serde_json::json!([
5561 {
5562 "properties": {"type": {"const": "subagent"}}
5563 },
5564 {
5565 "properties": {"type": {"const": "external_a2a"}},
5566 "anyOf": [
5567 {"required": ["id"]},
5568 {"required": ["external_agent_id"]}
5569 ]
5570 }
5571 ])
5572 );
5573 }
5574
5575 struct ExistingSpawnAgentCapability;
5576
5577 impl Capability for ExistingSpawnAgentCapability {
5578 fn id(&self) -> &str {
5579 "existing_spawn_agent"
5580 }
5581
5582 fn name(&self) -> &str {
5583 "Existing Spawn Agent"
5584 }
5585
5586 fn description(&self) -> &str {
5587 "Test capability that already owns spawn_agent"
5588 }
5589
5590 fn tools(&self) -> Vec<Box<dyn Tool>> {
5591 vec![Box::new(ExistingSpawnAgentTool)]
5592 }
5593 }
5594
5595 struct ExistingSpawnAgentTool;
5596
5597 #[async_trait]
5598 impl Tool for ExistingSpawnAgentTool {
5599 fn name(&self) -> &str {
5600 "spawn_agent"
5601 }
5602
5603 fn description(&self) -> &str {
5604 "Existing spawn_agent test tool"
5605 }
5606
5607 fn parameters_schema(&self) -> serde_json::Value {
5608 serde_json::json!({
5609 "type": "object",
5610 "properties": {
5611 "target": {
5612 "type": "object",
5613 "properties": {
5614 "type": {"type": "string", "enum": ["external_a2a"]}
5615 },
5616 "required": ["type"]
5617 }
5618 },
5619 "required": ["target"]
5620 })
5621 }
5622
5623 async fn execute(
5624 &self,
5625 _arguments: serde_json::Value,
5626 ) -> crate::tools::ToolExecutionResult {
5627 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5628 }
5629 }
5630
5631 #[tokio::test]
5632 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5633 let mut registry = CapabilityRegistry::new();
5634 registry.register(SubagentCapability);
5635 registry.register(ExistingSpawnAgentCapability);
5636
5637 let collected = collect_capabilities(
5638 &[
5639 SUBAGENTS_CAPABILITY_ID.to_string(),
5640 "existing_spawn_agent".to_string(),
5641 ],
5642 ®istry,
5643 &test_ctx(),
5644 )
5645 .await;
5646
5647 let spawn_agent_defs: Vec<_> = collected
5648 .tool_definitions
5649 .iter()
5650 .filter(|tool| tool.name() == "spawn_agent")
5651 .collect();
5652 assert_eq!(spawn_agent_defs.len(), 1);
5653 assert_eq!(
5654 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5655 serde_json::json!(["external_a2a"])
5656 );
5657 }
5658
5659 #[tokio::test]
5660 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5661 let mut registry = CapabilityRegistry::new();
5662 registry.register(AgentHandoffCapability);
5663 registry.register(ExistingSpawnAgentCapability);
5664
5665 let agent_id = crate::typed_id::AgentId::new();
5666 let harness_id = crate::typed_id::HarnessId::new();
5667 let configs = vec![
5668 AgentCapabilityConfig {
5669 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5670 config: serde_json::json!({
5671 "targets": [{
5672 "id": "aws_operator",
5673 "name": "AWS Operator",
5674 "agent_id": agent_id,
5675 "harness_id": harness_id
5676 }]
5677 }),
5678 },
5679 AgentCapabilityConfig {
5680 capability_ref: CapabilityId::new("existing_spawn_agent"),
5681 config: serde_json::json!({}),
5682 },
5683 ];
5684
5685 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5686
5687 let spawn_agent_defs: Vec<_> = collected
5688 .tool_definitions
5689 .iter()
5690 .filter(|tool| tool.name() == "spawn_agent")
5691 .collect();
5692 assert_eq!(spawn_agent_defs.len(), 1);
5693 assert_eq!(
5694 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5695 serde_json::json!(["external_a2a"])
5696 );
5697 }
5698
5699 #[tokio::test]
5703 async fn test_background_execution_explicit_selection_is_idempotent() {
5704 let registry = CapabilityRegistry::with_builtins();
5705 let collected = collect_capabilities(
5706 &[
5707 "bashkit_shell".to_string(),
5708 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5709 ],
5710 ®istry,
5711 &test_ctx(),
5712 )
5713 .await;
5714
5715 let spawn_background_count = collected
5716 .tool_definitions
5717 .iter()
5718 .filter(|t| t.name() == "spawn_background")
5719 .count();
5720 assert_eq!(
5721 spawn_background_count, 1,
5722 "spawn_background must appear exactly once even when \
5723 background_execution is selected explicitly alongside a \
5724 background-capable tool"
5725 );
5726 let applied_count = collected
5727 .applied_ids
5728 .iter()
5729 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5730 .count();
5731 assert_eq!(
5732 applied_count, 1,
5733 "background_execution must appear exactly once in applied_ids"
5734 );
5735 }
5736
5737 #[test]
5742 fn test_defaults_do_not_include_spawn_background() {
5743 let registry = crate::ToolRegistry::with_defaults();
5744 assert!(
5745 !registry.has("spawn_background"),
5746 "with_defaults() must not include 'spawn_background' — it comes \
5747 from the background_execution capability (EVE-501)"
5748 );
5749 }
5750
5751 #[test]
5756 fn test_capability_features_default_empty() {
5757 let registry = CapabilityRegistry::with_builtins();
5758
5759 let noop = registry.get("noop").unwrap();
5761 assert!(noop.features().is_empty());
5762
5763 let current_time = registry.get("current_time").unwrap();
5764 assert!(current_time.features().is_empty());
5765 }
5766
5767 #[test]
5768 fn test_file_system_capability_features() {
5769 let registry = CapabilityRegistry::with_builtins();
5770
5771 let fs = registry.get("session_file_system").unwrap();
5772 assert_eq!(fs.features(), vec!["file_system"]);
5773 }
5774
5775 #[test]
5776 fn test_bashkit_shell_capability_features() {
5777 let registry = CapabilityRegistry::with_builtins();
5778
5779 let bash = registry.get("bashkit_shell").unwrap();
5780 assert_eq!(bash.features(), vec!["file_system"]);
5781 }
5782
5783 #[test]
5784 fn test_alias_resolves_to_canonical_capability() {
5785 let registry = CapabilityRegistry::with_builtins();
5786
5787 let via_alias = registry.get("virtual_bash").unwrap();
5789 assert_eq!(via_alias.id(), "bashkit_shell");
5790 assert!(registry.has("virtual_bash"));
5791 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5792 assert_eq!(
5793 registry.canonical_id("bashkit_shell"),
5794 Some("bashkit_shell")
5795 );
5796 assert_eq!(registry.canonical_id("nonexistent"), None);
5797 }
5798
5799 #[test]
5800 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5801 let registry = CapabilityRegistry::with_builtins();
5802
5803 let resolved = resolve_dependencies(
5806 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5807 ®istry,
5808 )
5809 .unwrap();
5810 let bash_ids: Vec<_> = resolved
5811 .resolved_ids
5812 .iter()
5813 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5814 .collect();
5815 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5816 assert!(
5818 !resolved
5819 .added_as_dependencies
5820 .contains(&"bashkit_shell".to_string())
5821 );
5822 }
5823
5824 #[test]
5825 fn test_alias_preserves_explicit_config_in_resolution() {
5826 let registry = CapabilityRegistry::with_builtins();
5827
5828 let configs = vec![AgentCapabilityConfig::with_config(
5829 "virtual_bash".to_string(),
5830 serde_json::json!({"key": "value"}),
5831 )];
5832 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5833 let bash = resolved
5834 .iter()
5835 .find(|c| c.capability_id() == "bashkit_shell")
5836 .expect("alias must resolve to canonical bashkit_shell config");
5837 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5838 }
5839
5840 #[test]
5841 fn test_unregister_by_alias_removes_capability_and_aliases() {
5842 let mut registry = CapabilityRegistry::with_builtins();
5843
5844 assert!(registry.unregister("virtual_bash").is_some());
5845 assert!(!registry.has("bashkit_shell"));
5846 assert!(!registry.has("virtual_bash"));
5847 }
5848
5849 #[test]
5850 fn test_session_storage_capability_features() {
5851 let registry = CapabilityRegistry::with_builtins();
5852
5853 let storage = registry.get("session_storage").unwrap();
5854 let features = storage.features();
5855 assert!(features.contains(&"secrets"));
5856 assert!(features.contains(&"key_value"));
5857 }
5858
5859 #[test]
5860 fn test_session_schedule_capability_features() {
5861 let registry = CapabilityRegistry::with_builtins();
5862
5863 let schedule = registry.get("session_schedule").unwrap();
5864 assert_eq!(schedule.features(), vec!["schedules"]);
5865 }
5866
5867 #[test]
5868 fn test_session_sql_database_capability_features() {
5869 let registry = CapabilityRegistry::with_builtins();
5870
5871 let sql = registry.get("session_sql_database").unwrap();
5872 assert_eq!(sql.features(), vec!["sql_database"]);
5873 }
5874
5875 #[test]
5876 fn test_sample_data_capability_features() {
5877 let registry = CapabilityRegistry::with_builtins();
5878
5879 let sample = registry.get("sample_data").unwrap();
5880 assert_eq!(sample.features(), vec!["file_system"]);
5881 }
5882
5883 #[test]
5884 fn test_compute_features_empty() {
5885 let registry = CapabilityRegistry::with_builtins();
5886
5887 let features = compute_features(&[], ®istry);
5888 assert!(features.is_empty());
5889 }
5890
5891 #[test]
5892 fn test_compute_features_single_capability() {
5893 let registry = CapabilityRegistry::with_builtins();
5894
5895 let features = compute_features(&["session_schedule".to_string()], ®istry);
5896 assert_eq!(features, vec!["schedules"]);
5897 }
5898
5899 #[test]
5900 fn test_compute_features_multiple_capabilities() {
5901 let registry = CapabilityRegistry::with_builtins();
5902
5903 let features = compute_features(
5904 &[
5905 "session_file_system".to_string(),
5906 "session_storage".to_string(),
5907 "session_schedule".to_string(),
5908 ],
5909 ®istry,
5910 );
5911 assert!(features.contains(&"file_system".to_string()));
5912 assert!(features.contains(&"secrets".to_string()));
5913 assert!(features.contains(&"key_value".to_string()));
5914 assert!(features.contains(&"schedules".to_string()));
5915 }
5916
5917 #[test]
5918 fn test_compute_features_deduplicates() {
5919 let registry = CapabilityRegistry::with_builtins();
5920
5921 let features = compute_features(
5923 &[
5924 "session_file_system".to_string(),
5925 "bashkit_shell".to_string(),
5926 ],
5927 ®istry,
5928 );
5929 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5930 assert_eq!(file_system_count, 1, "file_system should appear only once");
5931 }
5932
5933 #[test]
5934 fn test_compute_features_includes_dependency_features() {
5935 let registry = CapabilityRegistry::with_builtins();
5936
5937 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5939 assert!(features.contains(&"file_system".to_string()));
5940 }
5941
5942 #[test]
5943 fn test_compute_features_generic_harness_set() {
5944 let registry = CapabilityRegistry::with_builtins();
5945
5946 let features = compute_features(
5948 &[
5949 "session_file_system".to_string(),
5950 "bashkit_shell".to_string(),
5951 "session_storage".to_string(),
5952 "session".to_string(),
5953 "session_schedule".to_string(),
5954 ],
5955 ®istry,
5956 );
5957 assert!(features.contains(&"file_system".to_string()));
5958 assert!(features.contains(&"secrets".to_string()));
5959 assert!(features.contains(&"key_value".to_string()));
5960 assert!(features.contains(&"schedules".to_string()));
5961 }
5962
5963 #[test]
5964 fn test_compute_features_unknown_capability_ignored() {
5965 let registry = CapabilityRegistry::with_builtins();
5966
5967 let features = compute_features(
5968 &["unknown_cap".to_string(), "session_schedule".to_string()],
5969 ®istry,
5970 );
5971 assert_eq!(features, vec!["schedules"]);
5972 }
5973
5974 #[test]
5975 fn test_risk_level_ordering() {
5976 assert!(RiskLevel::Low < RiskLevel::Medium);
5977 assert!(RiskLevel::Medium < RiskLevel::High);
5978 }
5979
5980 #[test]
5981 fn test_risk_level_serde_roundtrip() {
5982 let high = RiskLevel::High;
5983 let json = serde_json::to_string(&high).unwrap();
5984 assert_eq!(json, "\"high\"");
5985 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5986 assert_eq!(back, RiskLevel::High);
5987 }
5988
5989 #[test]
5990 fn test_capability_risk_levels() {
5991 let registry = CapabilityRegistry::with_builtins();
5992
5993 let bash = registry.get("bashkit_shell").unwrap();
5995 assert_eq!(bash.risk_level(), RiskLevel::High);
5996
5997 let fetch = registry.get("web_fetch").unwrap();
5999 assert_eq!(fetch.risk_level(), RiskLevel::High);
6000
6001 let noop = registry.get("noop").unwrap();
6003 assert_eq!(noop.risk_level(), RiskLevel::Low);
6004 }
6005
6006 #[tokio::test]
6011 async fn test_apply_capabilities_openai_tool_search() {
6012 let registry = CapabilityRegistry::with_builtins();
6013 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6014
6015 let applied = apply_capabilities(
6016 base_runtime_agent.clone(),
6017 &["openai_tool_search".to_string()],
6018 ®istry,
6019 &test_ctx(),
6020 )
6021 .await;
6022
6023 assert_eq!(
6025 applied.runtime_agent.system_prompt,
6026 base_runtime_agent.system_prompt
6027 );
6028 assert!(applied.tool_registry.is_empty());
6029 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
6030
6031 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6033 assert!(ts.enabled);
6034 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6035 }
6036
6037 #[tokio::test]
6038 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
6039 let registry = CapabilityRegistry::with_builtins();
6040 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6041
6042 let applied = apply_capabilities(
6043 base_runtime_agent,
6044 &[
6045 "current_time".to_string(),
6046 "openai_tool_search".to_string(),
6047 "test_math".to_string(),
6048 ],
6049 ®istry,
6050 &test_ctx(),
6051 )
6052 .await;
6053
6054 assert!(applied.tool_registry.has("get_current_time"));
6056 assert!(applied.tool_registry.has("add"));
6057 assert!(applied.tool_registry.has("subtract"));
6058 assert!(applied.tool_registry.has("multiply"));
6059 assert!(applied.tool_registry.has("divide"));
6060
6061 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6063 assert!(ts.enabled);
6064 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6065 }
6066
6067 #[tokio::test]
6068 async fn test_collect_capabilities_tool_search_custom_threshold() {
6069 let registry = CapabilityRegistry::with_builtins();
6070
6071 let configs = vec![AgentCapabilityConfig {
6072 capability_ref: CapabilityId::new("openai_tool_search"),
6073 config: serde_json::json!({"threshold": 5}),
6074 }];
6075
6076 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6077
6078 let ts = collected.tool_search.as_ref().unwrap();
6079 assert!(ts.enabled);
6080 assert_eq!(ts.threshold, 5);
6081 }
6082
6083 #[tokio::test]
6084 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
6085 let registry = CapabilityRegistry::with_builtins();
6086
6087 let configs = vec![
6088 AgentCapabilityConfig {
6089 capability_ref: CapabilityId::new("auto_tool_search"),
6090 config: serde_json::json!({"threshold": 2}),
6091 },
6092 AgentCapabilityConfig {
6093 capability_ref: CapabilityId::new("test_math"),
6094 config: serde_json::json!({}),
6095 },
6096 ];
6097
6098 let ctx = test_ctx().with_model("claude-3-5-haiku");
6102 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6103
6104 assert!(
6105 collected.tool_search.is_none(),
6106 "auto_tool_search must not set a hosted config on a non-native model"
6107 );
6108 assert!(
6109 collected
6110 .tools
6111 .iter()
6112 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6113 "auto_tool_search must contribute the client-side tool_search tool"
6114 );
6115 assert!(
6116 !collected.tool_definition_hooks.is_empty(),
6117 "auto_tool_search must contribute a client-side deferral hook"
6118 );
6119
6120 let mut transformed = collected.tool_definitions.clone();
6121 for hook in &collected.tool_definition_hooks {
6122 transformed = hook.transform(transformed);
6123 }
6124 let add_tool = transformed
6125 .iter()
6126 .find(|tool| tool.name() == "add")
6127 .expect("test_math contributes add");
6128 assert!(
6129 add_tool.parameters().get("properties").is_none(),
6130 "generic auto_tool_search must honor the configured threshold"
6131 );
6132 }
6133
6134 #[tokio::test]
6135 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
6136 let registry = CapabilityRegistry::with_builtins();
6137
6138 let configs = vec![AgentCapabilityConfig {
6139 capability_ref: CapabilityId::new("auto_tool_search"),
6140 config: serde_json::json!({"threshold": 7}),
6141 }];
6142
6143 let ctx = test_ctx().with_model("gpt-5.4");
6146 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6147
6148 let ts = collected
6149 .tool_search
6150 .as_ref()
6151 .expect("auto_tool_search must set a hosted config on a native model");
6152 assert!(ts.enabled);
6153 assert_eq!(ts.threshold, 7);
6154 assert!(
6155 !collected
6156 .tools
6157 .iter()
6158 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6159 "hosted mechanism must not contribute the client-side tool_search tool"
6160 );
6161 assert!(
6162 collected.tool_definition_hooks.is_empty(),
6163 "hosted mechanism must not contribute a client-side deferral hook"
6164 );
6165 }
6166
6167 #[tokio::test]
6168 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6169 let registry = CapabilityRegistry::with_builtins();
6170
6171 let configs = vec![AgentCapabilityConfig {
6172 capability_ref: CapabilityId::new("auto_tool_search"),
6173 config: serde_json::json!({"threshold": 9}),
6174 }];
6175
6176 let ctx = test_ctx().with_model("claude-opus-4-8");
6179 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6180
6181 let ts = collected
6182 .tool_search
6183 .as_ref()
6184 .expect("auto_tool_search must set a hosted config on a native Claude model");
6185 assert!(ts.enabled);
6186 assert_eq!(ts.threshold, 9);
6187 assert!(
6188 !collected
6189 .tools
6190 .iter()
6191 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6192 "hosted mechanism must not contribute the client-side tool_search tool"
6193 );
6194 assert!(
6195 collected.tool_definition_hooks.is_empty(),
6196 "hosted mechanism must not contribute a client-side deferral hook"
6197 );
6198 }
6199
6200 #[tokio::test]
6201 async fn test_collect_capabilities_no_tool_search_without_capability() {
6202 let registry = CapabilityRegistry::with_builtins();
6203
6204 let configs = vec![AgentCapabilityConfig {
6205 capability_ref: CapabilityId::new("current_time"),
6206 config: serde_json::json!({}),
6207 }];
6208
6209 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6210
6211 assert!(collected.tool_search.is_none());
6212 }
6213
6214 #[tokio::test]
6215 async fn test_collect_capabilities_tool_search_category_propagation() {
6216 let registry = CapabilityRegistry::with_builtins();
6217
6218 let configs = vec![
6220 AgentCapabilityConfig {
6221 capability_ref: CapabilityId::new("test_math"),
6222 config: serde_json::json!({}),
6223 },
6224 AgentCapabilityConfig {
6225 capability_ref: CapabilityId::new("openai_tool_search"),
6226 config: serde_json::json!({}),
6227 },
6228 ];
6229
6230 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6231
6232 assert!(collected.tool_search.is_some());
6234
6235 for tool_def in &collected.tool_definitions {
6237 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6239 assert!(
6240 tool_def.category().is_some(),
6241 "Tool {} should have a category from its capability",
6242 tool_def.name()
6243 );
6244 }
6245 }
6246 }
6247
6248 #[tokio::test]
6249 async fn test_apply_capabilities_prompt_caching() {
6250 let registry = CapabilityRegistry::with_builtins();
6251 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6252
6253 let applied = apply_capabilities(
6254 base_runtime_agent.clone(),
6255 &["prompt_caching".to_string()],
6256 ®istry,
6257 &test_ctx(),
6258 )
6259 .await;
6260
6261 assert_eq!(
6262 applied.runtime_agent.system_prompt,
6263 base_runtime_agent.system_prompt
6264 );
6265 assert!(applied.tool_registry.is_empty());
6266 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6267
6268 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6269 assert!(prompt_cache.enabled);
6270 assert_eq!(
6271 prompt_cache.strategy,
6272 crate::driver_registry::PromptCacheStrategy::Auto
6273 );
6274 assert!(prompt_cache.gemini_cached_content.is_none());
6275 }
6276
6277 #[tokio::test]
6278 async fn test_apply_capabilities_openrouter_server_tools() {
6279 let registry = CapabilityRegistry::with_builtins();
6280 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6281
6282 let configs = vec![AgentCapabilityConfig {
6283 capability_ref: CapabilityId::new("openrouter_server_tools"),
6284 config: serde_json::json!({
6285 "tools": ["web_search", "datetime"],
6286 "web_search_max_results": 4,
6287 }),
6288 }];
6289
6290 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6291 let routing = collected
6292 .openrouter_routing
6293 .as_ref()
6294 .expect("server tools produce routing config");
6295 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6296 assert_eq!(
6297 kinds,
6298 vec![
6299 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6300 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6301 ]
6302 );
6303
6304 let applied = apply_capabilities(
6307 base_runtime_agent,
6308 &["openrouter_server_tools".to_string()],
6309 ®istry,
6310 &test_ctx(),
6311 )
6312 .await;
6313 assert!(applied.tool_registry.is_empty());
6314 assert!(applied.runtime_agent.openrouter_routing.is_none());
6315 }
6316
6317 #[tokio::test]
6318 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6319 let registry = CapabilityRegistry::with_builtins();
6320
6321 let configs = vec![AgentCapabilityConfig {
6322 capability_ref: CapabilityId::new("prompt_caching"),
6323 config: serde_json::json!({"strategy": "auto"}),
6324 }];
6325
6326 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6327
6328 let prompt_cache = collected.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_collect_capabilities_prompt_caching_gemini_cached_content() {
6339 let registry = CapabilityRegistry::with_builtins();
6340
6341 let configs = vec![AgentCapabilityConfig {
6342 capability_ref: CapabilityId::new("prompt_caching"),
6343 config: serde_json::json!({
6344 "strategy": "auto",
6345 "gemini_cached_content": "cachedContents/demo-cache"
6346 }),
6347 }];
6348
6349 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6350
6351 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6352 assert_eq!(
6353 prompt_cache.gemini_cached_content.as_deref(),
6354 Some("cachedContents/demo-cache")
6355 );
6356 }
6357
6358 #[tokio::test]
6359 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6360 let registry = CapabilityRegistry::with_builtins();
6361
6362 let collected = collect_capabilities_with_configs(
6364 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6365 ®istry,
6366 &test_ctx(),
6367 )
6368 .await;
6369 assert_eq!(collected.parallel_tool_calls, Some(true));
6370
6371 let collected = collect_capabilities_with_configs(
6373 &[AgentCapabilityConfig {
6374 capability_ref: CapabilityId::new("parallel_tool_calls"),
6375 config: serde_json::json!({"mode": "avoid"}),
6376 }],
6377 ®istry,
6378 &test_ctx(),
6379 )
6380 .await;
6381 assert_eq!(collected.parallel_tool_calls, Some(false));
6382
6383 let collected = collect_capabilities_with_configs(
6385 &[AgentCapabilityConfig {
6386 capability_ref: CapabilityId::new("parallel_tool_calls"),
6387 config: serde_json::json!({"mode": "none"}),
6388 }],
6389 ®istry,
6390 &test_ctx(),
6391 )
6392 .await;
6393 assert_eq!(collected.parallel_tool_calls, None);
6394
6395 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6397 assert_eq!(collected.parallel_tool_calls, None);
6398 }
6399
6400 #[tokio::test]
6401 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6402 let registry = CapabilityRegistry::with_builtins();
6403
6404 let applied = apply_capabilities(
6406 RuntimeAgent::new("p", "gpt-5.2"),
6407 &["parallel_tool_calls".to_string()],
6408 ®istry,
6409 &test_ctx(),
6410 )
6411 .await;
6412 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6413
6414 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6416 base.parallel_tool_calls = Some(false);
6417 let applied = apply_capabilities(
6418 base,
6419 &["parallel_tool_calls".to_string()],
6420 ®istry,
6421 &test_ctx(),
6422 )
6423 .await;
6424 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6425 }
6426
6427 struct SkillContributingCapability;
6432
6433 impl Capability for SkillContributingCapability {
6434 fn id(&self) -> &str {
6435 "contributes_skills"
6436 }
6437 fn name(&self) -> &str {
6438 "Contributes Skills"
6439 }
6440 fn description(&self) -> &str {
6441 "Test capability that contributes skills."
6442 }
6443 fn contribute_skills(&self) -> Vec<SkillContribution> {
6444 vec![
6445 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6446 .with_files(vec![(
6447 "scripts/a.sh".to_string(),
6448 "#!/bin/sh\necho a\n".to_string(),
6449 )]),
6450 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6451 .with_user_invocable(false),
6452 ]
6453 }
6454 }
6455
6456 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6457 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6458 MountSource::InlineFile { content, .. } => content.as_str(),
6459 _ => panic!("Expected InlineFile for SKILL.md"),
6460 }
6461 }
6462
6463 #[tokio::test]
6464 async fn test_contribute_skills_normalized_to_mounts() {
6465 let mut registry = CapabilityRegistry::new();
6466 registry.register(SkillContributingCapability);
6467
6468 let configs = vec![AgentCapabilityConfig {
6469 capability_ref: CapabilityId::new("contributes_skills"),
6470 config: serde_json::json!({}),
6471 }];
6472
6473 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6474
6475 let skill_mounts: Vec<_> = collected
6476 .mounts
6477 .iter()
6478 .filter(|m| m.path.starts_with("/.agents/skills/"))
6479 .collect();
6480 assert_eq!(skill_mounts.len(), 2);
6481
6482 for m in &skill_mounts {
6485 assert!(m.is_readonly());
6486 assert_eq!(m.capability_id, "contributes_skills");
6487 }
6488
6489 let alpha = skill_mounts
6490 .iter()
6491 .find(|m| m.path == "/.agents/skills/alpha-skill")
6492 .expect("alpha-skill mount missing");
6493 match &alpha.source {
6494 MountSource::InlineDirectory { entries } => {
6495 assert!(entries.contains_key("SKILL.md"));
6496 assert!(entries.contains_key("scripts/a.sh"));
6497 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6498 assert_eq!(parsed.name, "alpha-skill");
6499 assert!(parsed.user_invocable);
6500 }
6501 _ => panic!("Expected InlineDirectory"),
6502 }
6503
6504 let beta = skill_mounts
6505 .iter()
6506 .find(|m| m.path == "/.agents/skills/beta-skill")
6507 .expect("beta-skill mount missing");
6508 match &beta.source {
6509 MountSource::InlineDirectory { entries } => {
6510 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6511 assert!(!parsed.user_invocable);
6512 }
6513 _ => panic!("Expected InlineDirectory"),
6514 }
6515 }
6516
6517 #[tokio::test]
6518 async fn test_contribute_skills_default_empty() {
6519 let mut registry = CapabilityRegistry::new();
6522 registry.register(FilterTestCapability { priority: 0 });
6523
6524 let configs = vec![AgentCapabilityConfig {
6525 capability_ref: CapabilityId::new("filter_test"),
6526 config: serde_json::json!({}),
6527 }];
6528
6529 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6530 assert!(
6531 collected
6532 .mounts
6533 .iter()
6534 .all(|m| !m.path.starts_with("/.agents/skills/"))
6535 );
6536 }
6537
6538 struct LocalizedCapability;
6539
6540 impl Capability for LocalizedCapability {
6541 fn id(&self) -> &str {
6542 "localized"
6543 }
6544 fn name(&self) -> &str {
6545 "Localized"
6546 }
6547 fn description(&self) -> &str {
6548 "English description"
6549 }
6550 fn localizations(&self) -> Vec<CapabilityLocalization> {
6551 vec![
6552 CapabilityLocalization {
6553 locale: "en",
6554 name: None,
6555 description: None,
6556 config_description: Some("Controls things."),
6557 config_overlay: None,
6558 },
6559 CapabilityLocalization {
6560 locale: "uk",
6561 name: Some("Локалізована"),
6562 description: Some("Український опис"),
6563 config_description: Some("Керує налаштуваннями."),
6564 config_overlay: None,
6565 },
6566 ]
6567 }
6568 }
6569
6570 #[test]
6571 fn localized_name_falls_back_exact_language_then_base() {
6572 let cap = LocalizedCapability;
6573 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6575 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6576 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6578 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6580 assert_eq!(cap.localized_name(None), "Localized");
6581 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6582 assert_eq!(cap.localized_description(Some("de")), "English description");
6583 }
6584
6585 #[test]
6586 fn describe_schema_resolves_config_description_per_locale() {
6587 let cap = LocalizedCapability;
6588 assert_eq!(
6589 cap.describe_schema(Some("uk-UA")).as_deref(),
6590 Some("Керує налаштуваннями.")
6591 );
6592 assert_eq!(
6594 cap.describe_schema(Some("pl")).as_deref(),
6595 Some("Controls things.")
6596 );
6597 assert_eq!(
6598 cap.describe_schema(None).as_deref(),
6599 Some("Controls things.")
6600 );
6601 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6603 }
6604}