1use crate::capability_types::is_plugin_capability;
22use crate::command::{
23 CommandDescriptor, CommandExecutionContext, CommandResult, ExecuteCommandRequest,
24};
25use crate::deployment::DeploymentGrade;
26use crate::events::TokenUsage;
27use crate::mcp_server::{ScopedMcpServers, merge_scoped_mcp_servers};
28use crate::message::Message;
29use crate::message_filter::MessageFilterProvider;
30use crate::runtime_agent::RuntimeAgent;
31use crate::tool_types::{ToolCall, ToolDefinition};
32use crate::tools::{Tool, ToolExecutionResult, ToolRegistry};
33use crate::traits::{SessionFileSystem, ToolContext};
34use crate::typed_id::SessionId;
35use async_trait::async_trait;
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::sync::Arc;
39
40pub struct IntegrationPlugin {
64 pub experimental_only: bool,
66 pub feature_flag: Option<&'static str>,
69 pub factory: fn() -> Box<dyn Capability>,
71}
72
73inventory::collect!(IntegrationPlugin);
74
75pub use crate::capability_types::{
77 AgentCapabilityConfig, CapabilityId, CapabilityStatus, MountAccess, MountDirectoryBuilder,
78 MountEntry, MountPoint, MountSource,
79};
80
81#[cfg(feature = "a2a")]
86mod a2a_delegation;
87#[cfg(feature = "ui-capabilities")]
88mod a2ui;
89mod agent_handoff;
90mod agent_instructions;
91pub mod attach_skill;
92mod auto_tool_search;
93mod background_execution;
94mod bashkit_shell;
95mod btw;
96mod budgeting;
97mod citation_retrieval;
98mod citation_verification;
99mod claude_tool_search;
100pub mod compaction;
101mod current_time;
102mod data_knowledge;
103mod declarative;
104mod delegation_result;
105mod error_disclosure;
106pub mod facts;
107mod fake_aws;
108mod fake_crm;
109mod fake_financial;
110mod fake_warehouse;
111mod file_system;
112mod guardrails;
113mod human_intent;
114mod infinity_context;
115mod knowledge_base;
116mod knowledge_index;
117mod loop_detection;
118mod lua;
119mod lua_code_mode;
120pub mod mcp;
121mod memory;
122mod message_metadata;
123mod model_scout;
124mod monitors;
125mod noop;
126mod openai_tool_search;
127mod openrouter_server_tools;
128mod openrouter_workspace;
129#[cfg(feature = "ui-capabilities")]
130mod openui;
131mod parallel_tool_calls;
132mod platform_management;
133mod progress_guard;
134mod prompt_caching;
135mod prompt_canary_guardrail;
136mod research;
137mod sample_data;
138mod self_budget;
139mod session;
140mod session_sandbox;
141mod session_schedule;
142mod session_sql_database;
143mod session_storage;
144mod session_tasks;
145mod skills;
146mod skills_scoped;
147mod stateless_todo_list;
148mod subagents;
149mod system_commands;
150mod test_math;
151mod test_weather;
152mod tool_call_repair;
153mod tool_output_distillation;
154mod tool_output_persistence;
155mod tool_search;
156mod usage_limit_auto_continue;
157pub mod user_hooks;
158mod util;
159#[cfg(feature = "web-fetch")]
160mod web_fetch;
161
162pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
167pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
171#[cfg(feature = "a2a")]
172pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
173#[cfg(feature = "ui-capabilities")]
174pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
175pub use agent_handoff::{
176 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
177};
178pub use agent_instructions::{
179 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
180 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
181 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
182};
183pub use attach_skill::{
184 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
185 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
186 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
187};
188pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
189pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
190pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
191pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
192pub use citation_retrieval::{
193 CITATION_RETRIEVAL_CAPABILITY_ID, CitationRetrievalCapability, CitationRetrievalConfig,
194};
195pub use citation_verification::{
196 CITATION_VERIFICATION_CAPABILITY_ID, CitationVerificationCapability,
197 CitationVerificationConfig, VerificationMode,
198};
199pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
200pub use compaction::{
201 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
202 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
203 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
204 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
205 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
206 build_summarization_prompt, build_summary_message, classify_memory_tiers,
207 compose_summary_with_recent, estimate_tokens, estimate_total_tokens,
208 format_messages_for_summarization, should_compact_for_cost, should_compact_proactively,
209 total_tool_result_bytes,
210};
211pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
212pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
213pub use declarative::{
214 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
215 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
216 declarative_capability_info, hydrate_declarative_capability_config,
217 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
218 plugin_capability_info, validate_declarative_capability_definition,
219};
220pub use delegation_result::{
221 ReportResultTool, ReportTaskProgressTool, report_result_tool_for_child_session,
222 report_task_progress_tool_for_child_session,
223};
224pub use error_disclosure::{
225 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
226};
227pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
228pub use fake_aws::{
229 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
230 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
231 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
232 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
233};
234pub use fake_crm::{
235 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
236 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
237 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
238};
239pub use fake_financial::{
240 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
241 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
242 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
243 FinanceListTransactionsTool,
244};
245pub use fake_warehouse::{
246 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
247 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
248 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
249 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
250};
251pub use file_system::{
252 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
253 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
254};
255pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
256pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
257pub use infinity_context::{
258 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, InfinityContextFilterOnlyCapability,
259 QueryHistoryTool,
260};
261pub use knowledge_base::{
262 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
263 validate_knowledge_base_config,
264};
265pub use knowledge_index::{
266 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
267 validate_knowledge_index_config,
268};
269pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
270pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
271pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
272pub use mcp::{
273 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
274 parse_mcp_capability_id,
275};
276pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
277pub use message_metadata::{
278 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
279 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
280};
281pub use model_scout::{
282 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
283 RouterUpdateProposal, compute_score, rank_results,
284};
285pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
286pub use openai_tool_search::{
287 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
288 model_supports_native_tool_search,
289};
290pub use openrouter_server_tools::{
291 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
292};
293pub use openrouter_workspace::{
294 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
295 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
296 detect_policy_drift,
297};
298#[cfg(feature = "ui-capabilities")]
299pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
300pub use parallel_tool_calls::{
301 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
302 parallel_tool_calls_from_config,
303};
304pub use platform_management::{
305 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
306 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
307 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
308};
309pub use progress_guard::{PROGRESS_GUARD_CAPABILITY_ID, ProgressGuardCapability};
310pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
311pub use prompt_canary_guardrail::{
312 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
313 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
314 REASON_CODE_SYSTEM_PROMPT_LEAK,
315};
316pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
317pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
318pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
319pub use session::{
320 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, SessionCapabilityConfig,
321 SessionTitleMutation, WriteSessionTitleTool, session_title_updated_event,
322 update_session_title_with_event,
323};
324pub use session_sandbox::{
325 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
326 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
327};
328pub use session_schedule::{
329 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
330 SessionScheduleCapability,
331};
332pub use session_sql_database::{
333 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
334 SqlSchemaTool,
335};
336pub use session_storage::{
337 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
338 is_internal_session_kv_key,
339};
340pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
341pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
342pub use skills_scoped::{
343 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
344};
345pub use stateless_todo_list::{
346 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
347};
348pub(crate) use subagents::SPAWN_AGENT_CONCURRENCY_CLASS;
349pub use subagents::{SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool, SubagentCapability};
350pub use usage_limit_auto_continue::{
351 AutoContinueConfig, USAGE_LIMIT_AUTO_CONTINUE_CAPABILITY_ID, UsageLimitAutoContinueCapability,
352 resolve_usage_limit_auto_continue,
353};
354pub use bashkit_shell::{
356 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
357};
358pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
359pub use test_math::{
360 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
361};
362pub use test_weather::{
363 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
364};
365pub use tool_call_repair::{
366 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
367 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
368 salvage_tool_arguments, tool_call_repair_capability,
369};
370pub use tool_output_distillation::{
371 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
372};
373pub use tool_output_persistence::{
374 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
375};
376pub use tool_search::{
377 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
378};
379pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
380#[cfg(feature = "web-fetch")]
381pub use web_fetch::{
382 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
383 derive_bot_auth_public_key,
384};
385
386pub struct SystemPromptContext {
396 pub session_id: SessionId,
398 pub locale: Option<String>,
400 pub file_store: Option<Arc<dyn SessionFileSystem>>,
402 pub model: Option<String>,
408}
409
410impl SystemPromptContext {
411 pub fn without_file_store(session_id: SessionId) -> Self {
413 Self {
414 session_id,
415 locale: None,
416 file_store: None,
417 model: None,
418 }
419 }
420
421 pub fn with_model(mut self, model: impl Into<String>) -> Self {
423 self.model = Some(model.into());
424 self
425 }
426}
427
428#[derive(Debug, Clone)]
480pub struct CapabilityLocalization {
481 pub locale: &'static str,
483 pub name: Option<&'static str>,
485 pub description: Option<&'static str>,
487 pub config_description: Option<&'static str>,
492 pub config_overlay: Option<serde_json::Value>,
498}
499
500impl CapabilityLocalization {
501 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
503 Self {
504 locale,
505 name: Some(name),
506 description: Some(description),
507 config_description: None,
508 config_overlay: None,
509 }
510 }
511}
512
513pub fn resolve_localized_field<T>(
517 localizations: &[CapabilityLocalization],
518 locale: Option<&str>,
519 field: impl Fn(&CapabilityLocalization) -> Option<T>,
520) -> Option<T> {
521 let mut candidates: Vec<String> = Vec::new();
522 if let Some(raw) = locale {
523 let normalized = raw.trim().replace('_', "-").to_lowercase();
524 if !normalized.is_empty() {
525 if let Some((language, _)) = normalized.split_once('-') {
526 let language = language.to_string();
527 candidates.push(normalized);
528 candidates.push(language);
529 } else {
530 candidates.push(normalized);
531 }
532 }
533 }
534 candidates.push("en".to_string());
535
536 for candidate in candidates {
537 let hit = localizations
538 .iter()
539 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
540 .and_then(&field);
541 if hit.is_some() {
542 return hit;
543 }
544 }
545 None
546}
547
548#[async_trait]
549pub trait Capability: Send + Sync {
550 fn id(&self) -> &str;
552
553 fn aliases(&self) -> Vec<&'static str> {
562 vec![]
563 }
564
565 fn name(&self) -> &str;
567
568 fn description(&self) -> &str;
570
571 fn localizations(&self) -> Vec<CapabilityLocalization> {
576 vec![]
577 }
578
579 fn localized_name(&self, locale: Option<&str>) -> String {
582 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
583 .unwrap_or_else(|| self.name())
584 .to_string()
585 }
586
587 fn localized_description(&self, locale: Option<&str>) -> String {
589 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
590 .unwrap_or_else(|| self.description())
591 .to_string()
592 }
593
594 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
598 resolve_localized_field(&self.localizations(), locale, |entry| {
599 entry.config_description
600 })
601 .map(str::to_string)
602 }
603
604 fn status(&self) -> CapabilityStatus {
606 CapabilityStatus::Available
607 }
608
609 fn icon(&self) -> Option<&str> {
611 None
612 }
613
614 fn category(&self) -> Option<&str> {
616 None
617 }
618
619 fn is_guardrail(&self) -> bool {
624 false
625 }
626
627 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
638 None
639 }
640
641 fn system_prompt_addition(&self) -> Option<&str> {
661 None
662 }
663
664 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
676 self.system_prompt_addition().map(|addition| {
677 format!(
678 "<capability id=\"{}\">\n{}\n</capability>",
679 self.id(),
680 addition
681 )
682 })
683 }
684
685 fn system_prompt_preview(&self) -> Option<String> {
691 self.system_prompt_addition().map(|s| s.to_string())
692 }
693
694 fn tools(&self) -> Vec<Box<dyn Tool>> {
696 vec![]
697 }
698
699 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
707 self.tools()
708 }
709
710 async fn system_prompt_contribution_with_config(
717 &self,
718 ctx: &SystemPromptContext,
719 _config: &serde_json::Value,
720 ) -> Option<String> {
721 self.system_prompt_contribution(ctx).await
722 }
723
724 fn tool_definitions(&self) -> Vec<ToolDefinition> {
727 self.tools().iter().map(|t| t.to_definition()).collect()
728 }
729
730 fn mounts(&self) -> Vec<MountPoint> {
738 vec![]
739 }
740
741 fn dependencies(&self) -> Vec<&'static str> {
750 vec![]
751 }
752
753 fn features(&self) -> Vec<&'static str> {
768 vec![]
769 }
770
771 fn config_schema(&self) -> Option<serde_json::Value> {
777 None
778 }
779
780 fn config_ui_schema(&self) -> Option<serde_json::Value> {
785 None
786 }
787
788 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
794 Ok(())
795 }
796
797 fn mcp_servers(&self) -> ScopedMcpServers {
803 ScopedMcpServers::default()
804 }
805
806 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
808 self.mcp_servers()
809 }
810
811 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
824 None
825 }
826
827 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
835 None
836 }
837
838 fn llm_error_hook(&self) -> Option<Arc<dyn crate::llm_error_hook::LlmErrorHook>> {
850 None
851 }
852
853 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
868 vec![]
869 }
870
871 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
882 vec![]
883 }
884
885 fn pre_tool_use_hooks_with_config(
890 &self,
891 _config: &serde_json::Value,
892 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
893 self.pre_tool_use_hooks()
894 }
895
896 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
904 vec![]
905 }
906
907 fn post_tool_exec_hooks_with_config(
912 &self,
913 _config: &serde_json::Value,
914 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
915 self.post_tool_exec_hooks()
916 }
917
918 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
927 vec![]
928 }
929
930 fn tool_definition_hooks_with_config(
935 &self,
936 _config: &serde_json::Value,
937 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
938 self.tool_definition_hooks()
939 }
940
941 fn tool_definition_hooks_with_context(
951 &self,
952 _ctx: &SystemPromptContext,
953 config: &serde_json::Value,
954 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
955 self.tool_definition_hooks_with_config(config)
956 }
957
958 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
966 vec![]
967 }
968
969 fn narrate(
983 &self,
984 _tool_def: Option<&ToolDefinition>,
985 tool_call: &ToolCall,
986 phase: crate::tool_narration::ToolNarrationPhase,
987 locale: Option<&str>,
988 ctx: crate::tool_narration::ToolNarrationContext<'_>,
989 ) -> Option<String> {
990 self.tools()
991 .iter()
992 .find(|tool| tool.name() == tool_call.name)
993 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
994 }
995
996 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
1012 vec![]
1013 }
1014
1015 fn user_hooks_with_config(
1021 &self,
1022 _config: &serde_json::Value,
1023 ) -> Vec<crate::user_hook_types::UserHookSpec> {
1024 self.user_hooks()
1025 }
1026
1027 fn risk_level(&self) -> RiskLevel {
1035 RiskLevel::Low
1036 }
1037
1038 fn commands(&self) -> Vec<CommandDescriptor> {
1046 vec![]
1047 }
1048
1049 async fn execute_command(
1063 &self,
1064 request: &ExecuteCommandRequest,
1065 _ctx: &CommandExecutionContext,
1066 ) -> crate::error::Result<CommandResult> {
1067 Err(crate::error::AgentLoopError::config(format!(
1068 "capability {} declared command /{} but does not implement execute_command",
1069 self.id(),
1070 request.name,
1071 )))
1072 }
1073
1074 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1083 vec![]
1084 }
1085
1086 fn contribute_skills(&self) -> Vec<SkillContribution> {
1096 vec![]
1097 }
1098
1099 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1110 vec![]
1111 }
1112
1113 fn post_output_guardrails_with_config(
1125 &self,
1126 _config: &serde_json::Value,
1127 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1128 vec![]
1129 }
1130
1131 fn post_output_annotation_hooks_with_config(
1147 &self,
1148 _config: &serde_json::Value,
1149 ) -> Vec<Arc<dyn crate::annotation_hook::PostGenerationAnnotationHook>> {
1150 vec![]
1151 }
1152
1153 fn citation_verifier_with_config(
1163 &self,
1164 _config: &serde_json::Value,
1165 ) -> Option<Arc<dyn crate::annotation_hook::CitationVerifier>> {
1166 None
1167 }
1168}
1169
1170pub trait ToolDefinitionHook: Send + Sync {
1171 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1172
1173 fn applies_with_native_tool_search(&self) -> bool {
1178 true
1179 }
1180}
1181
1182pub trait ToolCallHook: Send + Sync {
1183 fn narration(
1184 &self,
1185 _tool_def: Option<&ToolDefinition>,
1186 _tool_call: &ToolCall,
1187 _phase: crate::tool_narration::ToolNarrationPhase,
1188 _locale: Option<&str>,
1189 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
1190 ) -> Option<String> {
1191 None
1192 }
1193
1194 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1195 tool_call
1196 }
1197}
1198
1199pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1205
1206impl ToolCallHook for CapabilityNarrationHook {
1207 fn narration(
1208 &self,
1209 tool_def: Option<&ToolDefinition>,
1210 tool_call: &ToolCall,
1211 phase: crate::tool_narration::ToolNarrationPhase,
1212 locale: Option<&str>,
1213 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1214 ) -> Option<String> {
1215 self.0.narrate(tool_def, tool_call, phase, locale, ctx)
1216 }
1217}
1218
1219#[derive(
1223 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1224)]
1225#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1226#[cfg_attr(feature = "openapi", schema(example = "low"))]
1227#[serde(rename_all = "lowercase")]
1228pub enum RiskLevel {
1229 Low,
1231 Medium,
1233 High,
1235}
1236
1237#[derive(Debug, Clone, Serialize, Deserialize)]
1243#[serde(rename_all = "snake_case")]
1244pub enum BlueprintModel {
1245 Fixed(String),
1247 Default(String),
1249 Inherit,
1251}
1252
1253pub struct AgentBlueprint {
1259 pub id: &'static str,
1261 pub name: &'static str,
1263 pub description: &'static str,
1265 pub model: BlueprintModel,
1267 pub system_prompt: &'static str,
1269 pub tools: Vec<Box<dyn Tool>>,
1271 pub max_turns: Option<usize>,
1273 pub config_schema: Option<serde_json::Value>,
1275}
1276
1277impl AgentBlueprint {
1278 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1280 self.tools.iter().map(|t| t.to_definition()).collect()
1281 }
1282}
1283
1284impl std::fmt::Debug for AgentBlueprint {
1285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1286 f.debug_struct("AgentBlueprint")
1287 .field("id", &self.id)
1288 .field("name", &self.name)
1289 .field("model", &self.model)
1290 .field("tool_count", &self.tools.len())
1291 .field("max_turns", &self.max_turns)
1292 .finish()
1293 }
1294}
1295
1296#[derive(Clone)]
1323pub struct CapabilityRegistry {
1324 capabilities: HashMap<String, Arc<dyn Capability>>,
1325 aliases: HashMap<String, String>,
1327}
1328
1329impl CapabilityRegistry {
1330 pub fn new() -> Self {
1332 Self {
1333 capabilities: HashMap::new(),
1334 aliases: HashMap::new(),
1335 }
1336 }
1337
1338 pub fn with_builtins() -> Self {
1343 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1344 }
1345
1346 pub fn runtime_builtins() -> Self {
1357 let mut registry = Self::new();
1358
1359 registry.register(AgentInstructionsCapability);
1360 registry.register(HumanIntentCapability);
1361 registry.register(NoopCapability);
1362 registry.register(CurrentTimeCapability);
1363 registry.register(MessageMetadataCapability);
1364 registry.register(FileSystemCapability);
1365 registry.register(SessionStorageCapability);
1366 registry.register(SessionCapability);
1367 registry.register(StatelessTodoListCapability);
1368 #[cfg(feature = "web-fetch")]
1369 registry.register(WebFetchCapability::from_env());
1370 registry.register(BashkitShellCapability);
1371 registry.register(BtwCapability);
1372 registry.register(InfinityContextCapability);
1373 registry.register(budgeting::BudgetingCapability);
1374 registry.register(SelfBudgetCapability);
1375 registry.register(CompactionCapability);
1376 registry.register(ErrorDisclosureCapability);
1377 registry.register(OpenAiToolSearchCapability::new());
1378 registry.register(ClaudeToolSearchCapability::new());
1379 registry.register(ToolSearchCapability::new());
1380 registry.register(AutoToolSearchCapability::new());
1381 registry.register(PromptCachingCapability::new());
1382 registry.register(ParallelToolCallsCapability);
1383 registry.register(SkillsCapability);
1384 registry.register(SystemCommandsCapability);
1385 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1386 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1387 registry.register(LoopDetectionCapability);
1388 registry.register(ProgressGuardCapability::new());
1389 registry.register(ToolCallRepairCapability);
1390 registry.register(PromptCanaryGuardrailCapability);
1391 registry.register(GuardrailsCapability);
1392 registry.register(user_hooks::UserHooksCapability);
1393
1394 let internal_flags = crate::InternalFeatureFlags::from_env();
1395 if internal_flags.lua {
1396 registry.register(LuaCapability);
1397 registry.register(LuaCodeModeCapability);
1398 }
1399
1400 registry
1401 }
1402
1403 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1408 let mut registry = Self::new();
1409
1410 registry.register(AgentInstructionsCapability);
1412 registry.register(HumanIntentCapability);
1413 registry.register(NoopCapability);
1414 registry.register(CurrentTimeCapability);
1415 registry.register(MessageMetadataCapability);
1416 registry.register(ResearchCapability);
1417 registry.register(ModelScoutCapability);
1418 registry.register(OpenRouterWorkspaceCapability);
1419 registry.register(OpenRouterServerToolsCapability);
1420 registry.register(PlatformManagementCapability);
1421 registry.register(FileSystemCapability);
1422 registry.register(MemoryCapability);
1423 registry.register(SessionStorageCapability);
1424 registry.register(SessionCapability);
1425 registry.register(SessionSqlDatabaseCapability);
1426 registry.register(TestMathCapability);
1427 registry.register(TestWeatherCapability);
1428 registry.register(StatelessTodoListCapability);
1429 #[cfg(feature = "web-fetch")]
1430 registry.register(WebFetchCapability::from_env());
1431 registry.register(BashkitShellCapability);
1432 registry.register(BackgroundExecutionCapability);
1433 registry.register(SessionScheduleCapability);
1434 registry.register(BtwCapability);
1435 registry.register(InfinityContextCapability);
1436 registry.register(budgeting::BudgetingCapability);
1437 registry.register(SelfBudgetCapability);
1438 registry.register(CompactionCapability);
1439 registry.register(ErrorDisclosureCapability);
1440
1441 registry.register(OpenAiToolSearchCapability::new());
1443 registry.register(ClaudeToolSearchCapability::new());
1445 registry.register(ToolSearchCapability::new());
1447 registry.register(AutoToolSearchCapability::new());
1449 registry.register(PromptCachingCapability::new());
1450
1451 registry.register(ParallelToolCallsCapability);
1453
1454 registry.register(SkillsCapability);
1456
1457 registry.register(SubagentCapability);
1459
1460 registry.register(SessionTasksCapability);
1462
1463 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1467 registry.register(AgentHandoffCapability);
1468 #[cfg(feature = "a2a")]
1472 registry.register(A2aAgentDelegationCapability);
1473 }
1474
1475 registry.register(SystemCommandsCapability);
1477
1478 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1480 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1481
1482 registry.register(user_hooks::UserHooksCapability);
1485
1486 registry.register(LoopDetectionCapability);
1488
1489 registry.register(ProgressGuardCapability::new());
1493
1494 registry.register(UsageLimitAutoContinueCapability);
1501
1502 registry.register(ToolCallRepairCapability);
1506
1507 registry.register(PromptCanaryGuardrailCapability);
1510
1511 registry.register(GuardrailsCapability);
1514
1515 #[cfg(feature = "ui-capabilities")]
1517 {
1518 registry.register(OpenUiCapability);
1519 registry.register(A2UiCapability);
1520 }
1521
1522 registry.register(SampleDataCapability);
1524
1525 registry.register(DataKnowledgeCapability);
1527
1528 registry.register(KnowledgeBaseCapability);
1530
1531 registry.register(KnowledgeIndexCapability);
1533
1534 registry.register(CitationRetrievalCapability);
1536
1537 registry.register(CitationVerificationCapability);
1539
1540 registry.register(FakeWarehouseCapability);
1542 registry.register(FakeAwsCapability);
1543 registry.register(FakeCrmCapability);
1544 registry.register(FakeFinancialCapability);
1545
1546 let internal_flags = crate::InternalFeatureFlags::from_env();
1548 if internal_flags.session_sandbox {
1549 registry.register(SessionSandboxCapability);
1550 }
1551
1552 if internal_flags.lua {
1556 registry.register(LuaCapability);
1557 registry.register(LuaCodeModeCapability);
1560 }
1561 for plugin in inventory::iter::<IntegrationPlugin>() {
1562 if (!plugin.experimental_only || grade.experimental_features_enabled())
1563 && plugin
1564 .feature_flag
1565 .is_none_or(|f| internal_flags.is_enabled(f))
1566 {
1567 registry.register_boxed((plugin.factory)());
1568 }
1569 }
1570
1571 registry
1572 }
1573
1574 pub fn register(&mut self, capability: impl Capability + 'static) {
1576 self.register_arc(Arc::new(capability));
1577 }
1578
1579 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1581 self.register_arc(Arc::from(capability));
1582 }
1583
1584 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1586 let canonical = capability.id().to_string();
1587 for alias in capability.aliases() {
1588 self.aliases.insert(alias.to_string(), canonical.clone());
1589 }
1590 self.capabilities.insert(canonical, capability);
1591 }
1592
1593 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1595 self.capabilities
1596 .get(id)
1597 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1598 }
1599
1600 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1605 if self.capabilities.contains_key(id) {
1606 Some(id)
1607 } else {
1608 self.aliases
1609 .get(id)
1610 .filter(|c| self.capabilities.contains_key(*c))
1611 .map(String::as_str)
1612 }
1613 }
1614
1615 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1617 let canonical = self.canonical_id(id)?.to_string();
1618 let removed = self.capabilities.remove(&canonical);
1619 self.aliases.retain(|_, target| *target != canonical);
1620 removed
1621 }
1622
1623 pub fn has(&self, id: &str) -> bool {
1625 self.get(id).is_some()
1626 }
1627
1628 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1630 self.capabilities.values().collect()
1631 }
1632
1633 pub fn len(&self) -> usize {
1635 self.capabilities.len()
1636 }
1637
1638 pub fn is_empty(&self) -> bool {
1640 self.capabilities.is_empty()
1641 }
1642
1643 pub fn builder() -> CapabilityRegistryBuilder {
1645 CapabilityRegistryBuilder::new()
1646 }
1647
1648 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1652 for cap in self.capabilities.values() {
1653 for bp in cap.agent_blueprints() {
1654 if bp.id == id {
1655 return Some(bp);
1656 }
1657 }
1658 }
1659 None
1660 }
1661
1662 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1666 for (capability_id, cap) in &self.capabilities {
1667 for bp in cap.agent_blueprints() {
1668 if bp.id == id {
1669 return Some((capability_id.clone(), bp));
1670 }
1671 }
1672 }
1673 None
1674 }
1675
1676 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1678 self.capabilities
1679 .values()
1680 .flat_map(|cap| cap.agent_blueprints())
1681 .collect()
1682 }
1683}
1684
1685impl Default for CapabilityRegistry {
1686 fn default() -> Self {
1687 Self::with_builtins()
1688 }
1689}
1690
1691impl std::fmt::Debug for CapabilityRegistry {
1692 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1693 let ids: Vec<_> = self.capabilities.keys().collect();
1694 f.debug_struct("CapabilityRegistry")
1695 .field("capabilities", &ids)
1696 .finish()
1697 }
1698}
1699
1700pub struct CapabilityRegistryBuilder {
1702 registry: CapabilityRegistry,
1703}
1704
1705impl CapabilityRegistryBuilder {
1706 pub fn new() -> Self {
1708 Self {
1709 registry: CapabilityRegistry::new(),
1710 }
1711 }
1712
1713 pub fn with_builtins() -> Self {
1715 Self {
1716 registry: CapabilityRegistry::with_builtins(),
1717 }
1718 }
1719
1720 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1722 self.registry.register(capability);
1723 self
1724 }
1725
1726 pub fn build(self) -> CapabilityRegistry {
1728 self.registry
1729 }
1730}
1731
1732impl Default for CapabilityRegistryBuilder {
1733 fn default() -> Self {
1734 Self::new()
1735 }
1736}
1737
1738pub struct ModelViewContext<'a> {
1744 pub session_id: SessionId,
1745 pub prior_usage: Option<&'a TokenUsage>,
1746}
1747
1748pub trait ModelViewProvider: Send + Sync {
1754 fn apply_model_view(
1755 &self,
1756 messages: Vec<Message>,
1757 config: &serde_json::Value,
1758 context: &ModelViewContext<'_>,
1759 ) -> Vec<Message>;
1760
1761 fn priority(&self) -> i32 {
1762 0
1763 }
1764}
1765
1766pub struct CollectedCapabilities {
1771 pub system_prompt_parts: Vec<String>,
1773 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1775 pub tools: Vec<Box<dyn Tool>>,
1777 pub tool_definitions: Vec<ToolDefinition>,
1779 pub mounts: Vec<MountPoint>,
1781 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1783 pub applied_ids: Vec<String>,
1785 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1787 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1789 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1792 pub parallel_tool_calls: Option<bool>,
1796 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1798 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1800 pub mcp_servers: ScopedMcpServers,
1802 }
1808
1809#[derive(Debug, Clone, PartialEq, Eq)]
1810pub struct SystemPromptAttribution {
1811 pub capability_id: String,
1812 pub content: String,
1813}
1814
1815impl CollectedCapabilities {
1816 pub fn system_prompt_prefix(&self) -> Option<String> {
1819 if self.system_prompt_parts.is_empty() {
1820 None
1821 } else {
1822 Some(self.system_prompt_parts.join("\n\n"))
1823 }
1824 }
1825
1826 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1830 for (provider, config) in &self.message_filter_providers {
1832 provider.apply_filters(query, config);
1833 }
1834 }
1835
1836 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1839 for (provider, config) in &self.message_filter_providers {
1840 provider.post_load(messages, config);
1841 }
1842 }
1843
1844 pub fn has_message_filters(&self) -> bool {
1846 !self.message_filter_providers.is_empty()
1847 }
1848}
1849
1850struct SpawnAgentTargetProvider {
1851 target_type: &'static str,
1852 tool: Box<dyn Tool>,
1853}
1854
1855#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1857#[serde(rename_all = "snake_case")]
1858pub(crate) enum SpawnMode {
1859 Background,
1860 Foreground,
1861}
1862
1863impl SpawnMode {
1864 pub(crate) fn parse(value: &str) -> Option<Self> {
1865 match value {
1866 "background" => Some(Self::Background),
1867 "foreground" => Some(Self::Foreground),
1868 _ => None,
1869 }
1870 }
1871
1872 pub(crate) fn as_str(self) -> &'static str {
1873 match self {
1874 Self::Background => "background",
1875 Self::Foreground => "foreground",
1876 }
1877 }
1878}
1879
1880struct UnifiedSpawnAgentTool {
1881 providers: Vec<SpawnAgentTargetProvider>,
1882}
1883
1884impl UnifiedSpawnAgentTool {
1885 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1886 Self { providers }
1887 }
1888
1889 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1890 self.providers
1891 .iter()
1892 .find(|provider| provider.target_type == target_type)
1893 .map(|provider| provider.tool.as_ref())
1894 }
1895
1896 fn target_types(&self) -> Vec<&'static str> {
1897 ["subagent", "agent", "external_a2a"]
1898 .into_iter()
1899 .filter(|target_type| {
1900 self.providers
1901 .iter()
1902 .any(|provider| provider.target_type == *target_type)
1903 })
1904 .collect()
1905 }
1906
1907 fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
1912 self.target_types()
1913 .into_iter()
1914 .filter_map(|target_type| match target_type {
1915 "subagent" => Some(serde_json::json!({
1916 "properties": {
1917 "type": {"const": "subagent"}
1918 }
1919 })),
1920 "agent" => Some(serde_json::json!({
1921 "properties": {
1922 "type": {"const": "agent"}
1923 },
1924 "required": ["type", "id"]
1925 })),
1926 "external_a2a" => Some(serde_json::json!({
1927 "properties": {
1928 "type": {"const": "external_a2a"}
1929 },
1930 "anyOf": [
1931 {"required": ["id"]},
1932 {"required": ["external_agent_id"]}
1933 ]
1934 })),
1935 _ => None,
1936 })
1937 .collect()
1938 }
1939
1940 }
1950
1951#[async_trait]
1952impl Tool for UnifiedSpawnAgentTool {
1953 fn narrate(
1954 &self,
1955 tool_call: &ToolCall,
1956 phase: crate::tool_narration::ToolNarrationPhase,
1957 locale: Option<&str>,
1958 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1959 ) -> Option<String> {
1960 let target_type = tool_call
1961 .arguments
1962 .get("target")
1963 .and_then(|target| target.get("type"))
1964 .and_then(serde_json::Value::as_str)?;
1965 self.provider_for(target_type)
1966 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1967 }
1968
1969 fn name(&self) -> &str {
1970 "spawn_agent"
1971 }
1972
1973 fn display_name(&self) -> Option<&str> {
1974 Some("Spawn Agent")
1975 }
1976
1977 fn description(&self) -> &str {
1978 "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."
1979 }
1980
1981 fn parameters_schema(&self) -> serde_json::Value {
1982 serde_json::json!({
1983 "type": "object",
1984 "properties": {
1985 "name": {
1986 "type": "string",
1987 "description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
1988 },
1989 "instructions": {
1990 "type": "string",
1991 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
1992 },
1993 "goal": {
1994 "type": "string",
1995 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
1996 },
1997 "lifetime": {
1998 "type": "string",
1999 "enum": ["linked", "detached"],
2000 "default": "linked",
2001 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
2002 },
2003 "seed": {
2004 "type": "string",
2005 "enum": ["fresh", "fork", "workspace"],
2006 "default": "fresh",
2007 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
2008 },
2009 "target": {
2010 "type": "object",
2011 "properties": {
2012 "type": {
2013 "type": "string",
2014 "enum": self.target_types(),
2015 "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."
2016 },
2017 "id": {
2018 "type": "string",
2019 "description": "Configured target id for first-party handoffs or external A2A agents."
2020 },
2021 "external_agent_id": {
2022 "type": "string",
2023 "description": "Configured external A2A agent id."
2024 }
2025 },
2026 "required": ["type"],
2027 "oneOf": self.target_constraint_branches(),
2028 "additionalProperties": false
2029 },
2030 "mode": {
2031 "type": "string",
2032 "enum": ["background", "foreground"],
2033 "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."
2034 },
2035 "blueprint": {
2036 "type": "string",
2037 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
2038 },
2039 "config": {
2040 "type": "object",
2041 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
2042 },
2043 "result_schema": {
2044 "type": "object",
2045 "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."
2046 },
2047 "message_schema": {
2048 "type": "object",
2049 "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."
2050 },
2051 "public_context": {
2052 "type": "object",
2053 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
2054 },
2055 "wait_timeout_secs": {
2056 "type": "integer",
2057 "minimum": 1,
2058 "maximum": 86400,
2059 "description": "External-A2A-only foreground timeout."
2060 },
2061 "wake_on_completion": {
2062 "type": "boolean",
2063 "description": "External-A2A-only control for background completion wake-ups."
2064 }
2065 },
2066 "required": ["name", "instructions", "target"],
2067 "additionalProperties": false
2068 })
2069 }
2070
2071 fn hints(&self) -> crate::tool_types::ToolHints {
2072 let mut hints = crate::tool_types::ToolHints::default()
2073 .with_long_running(true)
2074 .with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
2075 if self.provider_for("external_a2a").is_some() {
2076 hints = hints.with_open_world(true);
2077 }
2078 hints
2079 }
2080
2081 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
2082 ToolExecutionResult::tool_error(
2083 "spawn_agent requires context. This tool must be executed with session context.",
2084 )
2085 }
2086
2087 async fn execute_with_context(
2088 &self,
2089 arguments: serde_json::Value,
2090 context: &ToolContext,
2091 ) -> ToolExecutionResult {
2092 let target_type = match arguments
2093 .get("target")
2094 .and_then(|target| target.get("type"))
2095 .and_then(serde_json::Value::as_str)
2096 {
2097 Some(target_type) => target_type,
2098 None => {
2099 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
2100 }
2101 };
2102
2103 let Some(provider) = self.provider_for(target_type) else {
2104 let supported = self.target_types().join(", ");
2105 return ToolExecutionResult::tool_error(format!(
2106 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
2107 ));
2108 };
2109 if target_type == "external_a2a"
2110 && arguments
2111 .get("lifetime")
2112 .and_then(serde_json::Value::as_str)
2113 .is_some_and(|value| value == "detached")
2114 {
2115 return ToolExecutionResult::tool_error(
2116 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2117 );
2118 }
2119 if target_type == "external_a2a"
2120 && arguments
2121 .get("message_schema")
2122 .is_some_and(|schema| !schema.is_null())
2123 {
2124 return ToolExecutionResult::tool_error(
2125 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2126 );
2127 }
2128
2129 provider.execute_with_context(arguments, context).await
2130 }
2131
2132 fn requires_context(&self) -> bool {
2133 true
2134 }
2135}
2136
2137pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2142 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2143 return base_system_prompt.to_string();
2144 };
2145
2146 if base_system_prompt.is_empty() {
2147 return additions.to_string();
2148 }
2149
2150 if base_system_prompt.contains("<system-prompt>") {
2151 format!("{base_system_prompt}\n\n{additions}")
2152 } else {
2153 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2154 }
2155}
2156
2157pub struct CollectedMessageFilters {
2164 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2166}
2167
2168pub struct CollectedModelViewProviders {
2170 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2172}
2173
2174impl CollectedMessageFilters {
2180 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2182 for (provider, config) in &self.message_filter_providers {
2183 provider.apply_filters(query, config);
2184 }
2185 }
2186
2187 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2189 for (provider, config) in &self.message_filter_providers {
2190 provider.post_load(messages, config);
2191 }
2192 }
2193}
2194
2195impl CollectedModelViewProviders {
2196 pub fn apply_model_view(
2198 &self,
2199 mut messages: Vec<Message>,
2200 context: &ModelViewContext<'_>,
2201 ) -> Vec<Message> {
2202 for (provider, config) in &self.model_view_providers {
2203 messages = provider.apply_model_view(messages, config, context);
2204 }
2205 messages
2206 }
2207}
2208
2209fn compaction_is_enabled(
2215 capability_configs: &[AgentCapabilityConfig],
2216 registry: &CapabilityRegistry,
2217) -> bool {
2218 capability_configs.iter().any(|cap_config| {
2219 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2220 && registry
2221 .get(cap_config.capability_ref.as_str())
2222 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2223 })
2224}
2225
2226fn message_filter_config_for(
2235 cap_id: &str,
2236 base: &serde_json::Value,
2237 compaction_on: bool,
2238) -> serde_json::Value {
2239 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2240 return base.clone();
2241 }
2242 let mut config = base.clone();
2243 match config.as_object_mut() {
2244 Some(map) => {
2245 map.insert(
2246 "compaction_active".to_string(),
2247 serde_json::Value::Bool(true),
2248 );
2249 }
2250 None => {
2251 config = serde_json::json!({ "compaction_active": true });
2252 }
2253 }
2254 config
2255}
2256
2257pub fn collect_message_filters_only(
2263 capability_configs: &[AgentCapabilityConfig],
2264 registry: &CapabilityRegistry,
2265) -> CollectedMessageFilters {
2266 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2267 Vec::new();
2268 let compaction_on = compaction_is_enabled(capability_configs, registry);
2269
2270 for cap_config in capability_configs {
2271 let cap_id = cap_config.capability_ref.as_str();
2272 if let Some(capability) = registry.get(cap_id) {
2273 if capability.status() != CapabilityStatus::Available {
2274 continue;
2275 }
2276 let effective: &dyn Capability = capability
2279 .resolve_for_model(None)
2280 .unwrap_or_else(|| capability.as_ref());
2281 if let Some(provider) = effective.message_filter_provider() {
2282 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2283 message_filter_providers.push((provider, config));
2284 }
2285 }
2286 }
2287
2288 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2289
2290 CollectedMessageFilters {
2291 message_filter_providers,
2292 }
2293}
2294
2295pub fn collect_model_view_providers(
2302 capability_configs: &[AgentCapabilityConfig],
2303 registry: &CapabilityRegistry,
2304 model: Option<&str>,
2305) -> CollectedModelViewProviders {
2306 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2307
2308 for cap_config in capability_configs {
2309 let cap_id = cap_config.capability_ref.as_str();
2310 if let Some(capability) = registry.get(cap_id) {
2311 if capability.status() != CapabilityStatus::Available {
2312 continue;
2313 }
2314 let effective: &dyn Capability = capability
2315 .resolve_for_model(model)
2316 .unwrap_or_else(|| capability.as_ref());
2317 if let Some(provider) = effective.model_view_provider() {
2318 model_view_providers.push((provider, cap_config.config.clone()));
2319 }
2320 }
2321 }
2322
2323 model_view_providers.sort_by_key(|(p, _)| p.priority());
2324
2325 CollectedModelViewProviders {
2326 model_view_providers,
2327 }
2328}
2329
2330pub fn collect_dynamic_facts(
2336 capability_configs: &[AgentCapabilityConfig],
2337 registry: &CapabilityRegistry,
2338 model: Option<&str>,
2339 ctx: &FactsContext,
2340) -> Vec<Fact> {
2341 let mut dynamic = Vec::new();
2342 for cap_config in capability_configs {
2343 let cap_id = cap_config.capability_ref.as_str();
2344 if let Some(capability) = registry.get(cap_id) {
2345 if capability.status() != CapabilityStatus::Available {
2346 continue;
2347 }
2348 let effective: &dyn Capability = capability
2349 .resolve_for_model(model)
2350 .unwrap_or_else(|| capability.as_ref());
2351 for fact in effective.facts(&cap_config.config, ctx) {
2352 if fact.volatility == Volatility::Dynamic {
2353 dynamic.push(fact);
2354 }
2355 }
2356 }
2357 }
2358 dynamic
2359}
2360
2361pub fn collect_capability_mcp_servers(
2362 capability_configs: &[AgentCapabilityConfig],
2363 registry: &CapabilityRegistry,
2364) -> ScopedMcpServers {
2365 let mut servers = ScopedMcpServers::default();
2366
2367 for cap_config in capability_configs {
2368 let cap_id = cap_config.capability_ref.as_str();
2369 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2372 if let Ok(definition) =
2373 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2374 {
2375 if definition.status != CapabilityStatus::Available {
2376 continue;
2377 }
2378 if let Some(contributed) = definition.mcp_servers {
2379 servers = merge_scoped_mcp_servers(&servers, &contributed);
2380 }
2381 }
2382 continue;
2383 }
2384 if let Some(capability) = registry.get(cap_id) {
2385 if capability.status() != CapabilityStatus::Available {
2386 continue;
2387 }
2388 servers = merge_scoped_mcp_servers(
2389 &servers,
2390 &capability.mcp_servers_with_config(&cap_config.config),
2391 );
2392 }
2393 }
2394
2395 servers
2396}
2397
2398pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2405
2406#[derive(Debug, Clone, PartialEq, Eq)]
2408pub enum DependencyError {
2409 CircularDependency {
2411 capability_id: String,
2413 chain: Vec<String>,
2415 },
2416 TooManyCapabilities {
2418 count: usize,
2420 max: usize,
2422 },
2423}
2424
2425impl std::fmt::Display for DependencyError {
2426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2427 match self {
2428 DependencyError::CircularDependency {
2429 capability_id,
2430 chain,
2431 } => {
2432 write!(
2433 f,
2434 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2435 capability_id,
2436 chain.join(" -> "),
2437 capability_id
2438 )
2439 }
2440 DependencyError::TooManyCapabilities { count, max } => {
2441 write!(
2442 f,
2443 "Too many capabilities after resolution: {} (max: {})",
2444 count, max
2445 )
2446 }
2447 }
2448 }
2449}
2450
2451impl std::error::Error for DependencyError {}
2452
2453#[derive(Debug, Clone)]
2455pub struct ResolvedCapabilities {
2456 pub resolved_ids: Vec<String>,
2459 pub added_as_dependencies: Vec<String>,
2461 pub user_selected: Vec<String>,
2463}
2464
2465pub fn resolve_dependencies(
2485 selected_ids: &[String],
2486 registry: &CapabilityRegistry,
2487) -> Result<ResolvedCapabilities, DependencyError> {
2488 use std::collections::HashSet;
2489
2490 let user_selected: HashSet<String> = selected_ids
2492 .iter()
2493 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2494 .collect();
2495 let mut resolved: Vec<String> = Vec::new();
2496 let mut resolved_set: HashSet<String> = HashSet::new();
2497 let mut added_as_dependencies: Vec<String> = Vec::new();
2498
2499 for cap_id in selected_ids {
2501 resolve_single_capability(
2502 cap_id,
2503 registry,
2504 &mut resolved,
2505 &mut resolved_set,
2506 &mut added_as_dependencies,
2507 &user_selected,
2508 &mut Vec::new(), )?;
2510 }
2511
2512 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2514 return Err(DependencyError::TooManyCapabilities {
2515 count: resolved.len(),
2516 max: MAX_RESOLVED_CAPABILITIES,
2517 });
2518 }
2519
2520 Ok(ResolvedCapabilities {
2521 resolved_ids: resolved,
2522 added_as_dependencies,
2523 user_selected: selected_ids.to_vec(),
2524 })
2525}
2526
2527pub fn resolve_capability_configs(
2532 selected_configs: &[AgentCapabilityConfig],
2533 registry: &CapabilityRegistry,
2534) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2535 let mut selected_ids: Vec<String> = Vec::new();
2536 for config in selected_configs {
2537 if (is_declarative_capability(config.capability_id())
2540 || is_plugin_capability(config.capability_id()))
2541 && let Ok(definition) =
2542 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2543 {
2544 selected_ids.extend(definition.dependencies);
2545 }
2546 selected_ids.push(config.capability_id().to_string());
2547 }
2548 let resolved = resolve_dependencies(&selected_ids, registry)?;
2549
2550 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2553 .iter()
2554 .map(|config| {
2555 let id = config.capability_id();
2556 let id = registry.canonical_id(id).unwrap_or(id);
2557 (id.to_string(), config.config.clone())
2558 })
2559 .collect();
2560
2561 Ok(resolved
2562 .resolved_ids
2563 .into_iter()
2564 .map(|capability_id| {
2565 explicit_configs
2566 .get(&capability_id)
2567 .cloned()
2568 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2569 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2570 })
2571 .collect())
2572}
2573
2574fn resolve_single_capability(
2576 cap_id: &str,
2577 registry: &CapabilityRegistry,
2578 resolved: &mut Vec<String>,
2579 resolved_set: &mut std::collections::HashSet<String>,
2580 added_as_dependencies: &mut Vec<String>,
2581 user_selected: &std::collections::HashSet<String>,
2582 visiting: &mut Vec<String>,
2583) -> Result<(), DependencyError> {
2584 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2588
2589 if resolved_set.contains(cap_id) {
2591 return Ok(());
2592 }
2593
2594 if visiting.contains(&cap_id.to_string()) {
2596 return Err(DependencyError::CircularDependency {
2597 capability_id: cap_id.to_string(),
2598 chain: visiting.clone(),
2599 });
2600 }
2601
2602 let capability = match registry.get(cap_id) {
2604 Some(cap) => cap,
2605 None => {
2606 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2610 && !resolved_set.contains(cap_id)
2611 {
2612 resolved.push(cap_id.to_string());
2613 resolved_set.insert(cap_id.to_string());
2614 if !user_selected.contains(cap_id) {
2615 added_as_dependencies.push(cap_id.to_string());
2616 }
2617 }
2618 return Ok(());
2619 }
2620 };
2621
2622 visiting.push(cap_id.to_string());
2624
2625 for dep_id in capability.dependencies() {
2627 resolve_single_capability(
2628 dep_id,
2629 registry,
2630 resolved,
2631 resolved_set,
2632 added_as_dependencies,
2633 user_selected,
2634 visiting,
2635 )?;
2636 }
2637
2638 visiting.pop();
2640
2641 if !resolved_set.contains(cap_id) {
2643 resolved.push(cap_id.to_string());
2644 resolved_set.insert(cap_id.to_string());
2645
2646 if !user_selected.contains(cap_id) {
2648 added_as_dependencies.push(cap_id.to_string());
2649 }
2650 }
2651
2652 Ok(())
2653}
2654
2655pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2660 use std::collections::HashSet;
2661
2662 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2663 Ok(resolved) => resolved.resolved_ids,
2664 Err(_) => capability_ids.to_vec(),
2665 };
2666
2667 let mut seen = HashSet::new();
2668 let mut features = Vec::new();
2669 for cap_id in &resolved_ids {
2670 if let Some(cap) = registry.get(cap_id) {
2671 for feature in cap.features() {
2672 if seen.insert(feature) {
2673 features.push(feature.to_string());
2674 }
2675 }
2676 }
2677 }
2678 features
2679}
2680
2681pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2684 registry
2685 .get(cap_id)
2686 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2687 .unwrap_or_default()
2688}
2689
2690pub async fn collect_capabilities(
2706 capability_ids: &[String],
2707 registry: &CapabilityRegistry,
2708 ctx: &SystemPromptContext,
2709) -> CollectedCapabilities {
2710 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2713 Ok(resolved) => resolved.resolved_ids,
2714 Err(e) => {
2715 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2716 capability_ids.to_vec()
2717 }
2718 };
2719
2720 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2722 .iter()
2723 .map(|id| AgentCapabilityConfig {
2724 capability_ref: CapabilityId::new(id),
2725 config: serde_json::Value::Object(serde_json::Map::new()),
2726 })
2727 .collect();
2728
2729 collect_capabilities_with_configs(&configs, registry, ctx).await
2730}
2731
2732pub async fn collect_capabilities_with_configs(
2743 capability_configs: &[AgentCapabilityConfig],
2744 registry: &CapabilityRegistry,
2745 ctx: &SystemPromptContext,
2746) -> CollectedCapabilities {
2747 let mut system_prompt_parts: Vec<String> = Vec::new();
2748 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2749 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2750 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2751 let mut mounts: Vec<MountPoint> = Vec::new();
2752 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2753 Vec::new();
2754 let mut applied_ids: Vec<String> = Vec::new();
2755 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2756 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2757 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2758 let mut parallel_tool_calls: Option<bool> = None;
2759 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2760 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2761 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2764 let mut mcp_servers = ScopedMcpServers::default();
2765 let mut static_facts: Vec<Fact> = Vec::new();
2769 let mut has_dynamic_facts = false;
2770 let facts_ctx = FactsContext::new(ctx.session_id);
2771 let compaction_on = compaction_is_enabled(capability_configs, registry);
2772 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2773 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2774
2775 for cap_config in capability_configs {
2776 let cap_id = cap_config.capability_ref.as_str();
2777 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2782 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2783 cap_config.config.clone(),
2784 ) {
2785 Ok(definition) => {
2786 if definition.status != CapabilityStatus::Available {
2787 continue;
2788 }
2789
2790 if let Some(prompt) = definition.system_prompt.as_deref() {
2791 let contribution =
2792 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2793 system_prompt_attributions.push(SystemPromptAttribution {
2794 capability_id: cap_id.to_string(),
2795 content: contribution.clone(),
2796 });
2797 system_prompt_parts.push(contribution);
2798 }
2799
2800 mounts.extend(definition.mounts(cap_id));
2801 if let Some(ref servers) = definition.mcp_servers {
2802 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2803 }
2804 for skill in definition.skill_contributions() {
2805 mounts.push(skill.to_mount(cap_id));
2806 }
2807
2808 applied_ids.push(cap_id.to_string());
2809 }
2810 Err(error) => {
2811 tracing::warn!(
2812 capability_id = %cap_id,
2813 error = %error,
2814 "Skipping invalid declarative/plugin capability config"
2815 );
2816 }
2817 }
2818 continue;
2819 }
2820 if let Some(capability) = registry.get(cap_id) {
2821 if capability.status() != CapabilityStatus::Available {
2823 continue;
2824 }
2825
2826 let effective: &dyn Capability =
2838 match capability.resolve_for_model(ctx.model.as_deref()) {
2839 Some(inner) => inner,
2840 None => capability.as_ref(),
2841 };
2842 let effective_id = effective.id();
2843 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2844 agent_handoff_spawn_config = Some(cap_config.config.clone());
2845 }
2846
2847 if let Some(contribution) = effective
2849 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2850 .await
2851 {
2852 system_prompt_attributions.push(SystemPromptAttribution {
2853 capability_id: cap_id.to_string(),
2854 content: contribution.clone(),
2855 });
2856 system_prompt_parts.push(contribution);
2857 }
2858
2859 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2864 match fact.volatility {
2865 Volatility::Static => static_facts.push(fact),
2866 Volatility::Dynamic => has_dynamic_facts = true,
2867 }
2868 }
2869
2870 for tool in effective.tools_with_config(&cap_config.config) {
2872 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2873 spawn_agent_providers.push(SpawnAgentTargetProvider {
2874 target_type: "external_a2a",
2875 tool,
2876 });
2877 } else {
2878 tools.push(tool);
2879 }
2880 }
2881 tool_definition_hooks
2882 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2883 tool_call_hooks.extend(effective.tool_call_hooks());
2884 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2886 let cap_category = effective.category();
2891 for def in effective.tool_definitions() {
2892 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2893 continue;
2894 }
2895 let def = match (def.category(), cap_category) {
2896 (None, Some(cat)) => def.with_category(cat),
2897 _ => def,
2898 }
2899 .with_capability_attribution(cap_id, Some(capability.name()));
2900 tool_definitions.push(def);
2901 }
2902
2903 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2911 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2912 {
2913 let threshold = cap_config
2915 .config
2916 .get("threshold")
2917 .and_then(|v| v.as_u64())
2918 .map(|v| v as usize)
2919 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2920 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2921 enabled: true,
2922 threshold,
2923 });
2924 }
2925
2926 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2927 let strategy = cap_config
2928 .config
2929 .get("strategy")
2930 .and_then(|v| v.as_str())
2931 .map(|value| match value {
2932 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2933 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2934 })
2935 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2936 let gemini_cached_content = cap_config
2937 .config
2938 .get("gemini_cached_content")
2939 .and_then(|v| v.as_str())
2940 .map(str::to_string);
2941 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2942 enabled: true,
2943 strategy,
2944 gemini_cached_content,
2945 });
2946 }
2947
2948 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2949 parallel_tool_calls =
2950 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2951 }
2952
2953 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2954 let server_tools =
2955 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2956 if !server_tools.is_empty() {
2957 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2958 server_tools,
2959 ..Default::default()
2960 });
2961 }
2962 }
2963
2964 mounts.extend(effective.mounts());
2966
2967 mcp_servers = merge_scoped_mcp_servers(
2968 &mcp_servers,
2969 &effective.mcp_servers_with_config(&cap_config.config),
2970 );
2971
2972 for skill in effective.contribute_skills() {
2976 mounts.push(skill.to_mount(cap_id));
2977 }
2978
2979 if let Some(provider) = effective.message_filter_provider() {
2981 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2982 message_filter_providers.push((provider, config));
2983 }
2984
2985 applied_ids.push(cap_id.to_string());
2986 }
2987 }
2988
2989 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
2994 spawn_agent_providers.push(SpawnAgentTargetProvider {
2995 target_type: "subagent",
2996 tool: Box::new(SpawnSubagentAsAgentTool),
2997 });
2998 }
2999 if let Some(config) = agent_handoff_spawn_config.as_ref() {
3000 spawn_agent_providers.push(SpawnAgentTargetProvider {
3001 target_type: "agent",
3002 tool: Box::new(SpawnAgentHandoffTool::new(config)),
3003 });
3004 }
3005 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
3006 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
3007 let def = tool
3008 .to_definition()
3009 .with_category("Orchestration")
3010 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
3011 tools.push(Box::new(tool));
3012 tool_definitions.push(def);
3013 }
3014
3015 if !applied_ids
3027 .iter()
3028 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
3029 && tool_definitions
3030 .iter()
3031 .any(|def| def.hints().supports_background == Some(true))
3032 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
3033 && bg_cap.status() == CapabilityStatus::Available
3034 {
3035 tools.extend(bg_cap.tools());
3036 let cap_category = bg_cap.category();
3037 for def in bg_cap.tool_definitions() {
3038 let def = match (def.category(), cap_category) {
3039 (None, Some(cat)) => def.with_category(cat),
3040 _ => def,
3041 }
3042 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
3043 tool_definitions.push(def);
3044 }
3045 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
3046 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
3047 }
3048
3049 if let Some(block) = facts::render_facts_block(&static_facts) {
3054 system_prompt_attributions.push(SystemPromptAttribution {
3055 capability_id: "facts".to_string(),
3056 content: block.clone(),
3057 });
3058 system_prompt_parts.push(block);
3059 }
3060 if has_dynamic_facts {
3061 system_prompt_attributions.push(SystemPromptAttribution {
3062 capability_id: "facts".to_string(),
3063 content: FACTS_DYNAMIC_NOTE.to_string(),
3064 });
3065 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
3066 }
3067
3068 tool_call_hooks.extend(narration_hooks);
3072
3073 message_filter_providers.sort_by_key(|(p, _)| p.priority());
3075
3076 CollectedCapabilities {
3077 system_prompt_parts,
3078 system_prompt_attributions,
3079 tools,
3080 tool_definitions,
3081 mounts,
3082 message_filter_providers,
3083 applied_ids,
3084 tool_search,
3085 prompt_cache,
3086 openrouter_routing,
3087 parallel_tool_calls,
3088 tool_definition_hooks,
3089 tool_call_hooks,
3090 mcp_servers,
3091 }
3092}
3093
3094pub struct AppliedCapabilities {
3100 pub runtime_agent: RuntimeAgent,
3102 pub tool_registry: ToolRegistry,
3104 pub applied_ids: Vec<String>,
3106}
3107
3108pub async fn apply_capabilities(
3145 base_runtime_agent: RuntimeAgent,
3146 capability_ids: &[String],
3147 registry: &CapabilityRegistry,
3148 ctx: &SystemPromptContext,
3149) -> AppliedCapabilities {
3150 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3151
3152 let final_system_prompt = compose_system_prompt(
3154 &base_runtime_agent.system_prompt,
3155 collected.system_prompt_prefix().as_deref(),
3156 );
3157
3158 let mut tool_registry = ToolRegistry::new();
3160 for tool in collected.tools {
3161 tool_registry.register_boxed(tool);
3162 }
3163
3164 let mut tools = collected.tool_definitions;
3166 for hook in &collected.tool_definition_hooks {
3167 tools = hook.transform(tools);
3168 }
3169
3170 let runtime_agent = RuntimeAgent {
3171 system_prompt: final_system_prompt,
3172 model: base_runtime_agent.model,
3173 tools,
3174 max_iterations: base_runtime_agent.max_iterations,
3175 temperature: base_runtime_agent.temperature,
3176 max_tokens: base_runtime_agent.max_tokens,
3177 tool_search: collected.tool_search,
3178 prompt_cache: collected.prompt_cache,
3179 openrouter_routing: collected.openrouter_routing,
3180 network_access: base_runtime_agent.network_access,
3181 parallel_tool_calls: base_runtime_agent
3184 .parallel_tool_calls
3185 .or(collected.parallel_tool_calls),
3186 };
3187
3188 AppliedCapabilities {
3189 runtime_agent,
3190 tool_registry,
3191 applied_ids: collected.applied_ids,
3192 }
3193}
3194
3195#[cfg(test)]
3200mod tests {
3201 use super::*;
3202 use crate::typed_id::SessionId;
3203 use std::collections::BTreeSet;
3204 use uuid::Uuid;
3205
3206 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3208
3209 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3210 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3211 }
3212
3213 fn test_ctx() -> SystemPromptContext {
3215 SystemPromptContext::without_file_store(SessionId::new())
3216 }
3217
3218 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3220 let mut ids = [
3221 "agent_instructions",
3222 "human_intent",
3223 "budgeting",
3224 "self_budget",
3225 "noop",
3226 "current_time",
3227 "research",
3228 "platform_management",
3229 "session_file_system",
3230 "session_storage",
3231 "session",
3232 "session_sql_database",
3233 "test_math",
3234 "test_weather",
3235 "stateless_todo_list",
3236 "web_fetch",
3237 "bashkit_shell",
3238 "background_execution",
3239 "session_schedule",
3240 "btw",
3241 "infinity_context",
3242 "compaction",
3243 "memory",
3244 "message_metadata",
3245 "openai_tool_search",
3246 "claude_tool_search",
3247 "tool_search",
3248 "auto_tool_search",
3249 "prompt_caching",
3250 "parallel_tool_calls",
3251 "session_tasks",
3252 "skills",
3253 "subagents",
3254 "system_commands",
3255 "sample_data",
3256 "data_knowledge",
3257 "knowledge_base",
3258 "knowledge_index",
3259 "citation_retrieval",
3260 "citation_verification",
3261 "tool_output_persistence",
3262 "tool_output_distillation",
3263 "fake_warehouse",
3264 "fake_aws",
3265 "fake_crm",
3266 "fake_financial",
3267 "loop_detection",
3268 "progress_guard",
3269 "usage_limit_auto_continue",
3270 "tool_call_repair",
3271 "error_disclosure",
3272 "prompt_canary_guardrail",
3273 "guardrails",
3274 "user_hooks",
3275 "model_scout",
3276 "openrouter_workspace",
3277 "openrouter_server_tools",
3278 ]
3279 .into_iter()
3280 .collect::<BTreeSet<_>>();
3281 if cfg!(feature = "ui-capabilities") {
3282 ids.insert("openui");
3283 ids.insert("a2ui");
3284 }
3285 ids
3286 }
3287
3288 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3290 let mut ids = [
3291 "agent_instructions",
3292 "human_intent",
3293 "budgeting",
3294 "self_budget",
3295 "noop",
3296 "current_time",
3297 "session_file_system",
3298 "session_storage",
3299 "session",
3300 "stateless_todo_list",
3301 "bashkit_shell",
3302 "btw",
3303 "infinity_context",
3304 "compaction",
3305 "message_metadata",
3306 "openai_tool_search",
3307 "claude_tool_search",
3308 "tool_search",
3309 "auto_tool_search",
3310 "prompt_caching",
3311 "parallel_tool_calls",
3312 "skills",
3313 "system_commands",
3314 "tool_output_persistence",
3315 "tool_output_distillation",
3316 "loop_detection",
3317 "progress_guard",
3318 "tool_call_repair",
3319 "error_disclosure",
3320 "prompt_canary_guardrail",
3321 "guardrails",
3322 "user_hooks",
3323 ]
3324 .into_iter()
3325 .collect::<BTreeSet<_>>();
3326 if cfg!(feature = "web-fetch") {
3327 ids.insert("web_fetch");
3328 }
3329 ids
3330 }
3331
3332 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3334 let mut ids = expected_core_builtin_ids();
3335 ids.insert("agent_handoff");
3336 ids.insert("a2a_agent_delegation");
3337 ids
3338 }
3339
3340 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3341 registry.capabilities.keys().map(String::as_str).collect()
3342 }
3343
3344 #[test]
3354 fn test_capability_registry_with_builtins_dev() {
3355 let _lock = lock_env();
3357 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3358 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3359 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3360 assert!(registry.has("agent_handoff"));
3361 assert!(registry.has("a2a_agent_delegation"));
3362 }
3363
3364 #[test]
3365 fn test_capability_registry_with_builtins_prod() {
3366 let _lock = lock_env();
3368 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3369 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3370 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3371 assert!(!registry.has("docker_container"));
3373 assert!(!registry.has("agent_handoff"));
3374 assert!(!registry.has("a2a_agent_delegation"));
3375 }
3376
3377 #[test]
3378 fn test_capability_registry_runtime_builtins() {
3379 let _lock = lock_env();
3380 unsafe { std::env::remove_var("FEATURE_LUA") };
3381 let registry = CapabilityRegistry::runtime_builtins();
3382 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3383 assert!(registry.has("session_file_system"));
3384 #[cfg(feature = "web-fetch")]
3385 assert!(registry.has("web_fetch"));
3386 assert!(registry.has("bashkit_shell"));
3387
3388 for platform_only in [
3389 "platform_management",
3390 "model_scout",
3391 "openrouter_workspace",
3392 "openrouter_server_tools",
3393 "session_tasks",
3394 "session_schedule",
3395 "subagents",
3396 "background_execution",
3397 "session_sql_database",
3398 "knowledge_base",
3399 "knowledge_index",
3400 "sample_data",
3401 "data_knowledge",
3402 "fake_aws",
3403 "fake_crm",
3404 "fake_financial",
3405 "fake_warehouse",
3406 "test_math",
3407 "test_weather",
3408 "research",
3409 ] {
3410 assert!(
3411 !registry.has(platform_only),
3412 "`{platform_only}` should not be in the runtime default registry"
3413 );
3414 }
3415 }
3416
3417 #[test]
3418 fn test_agent_delegation_enabled_by_env_in_prod() {
3419 let _lock = lock_env();
3421 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3422 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
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_agent_delegation_disabled_by_env_in_dev() {
3430 let _lock = lock_env();
3432 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3433 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3434 assert!(!registry.has("agent_handoff"));
3435 assert!(!registry.has("a2a_agent_delegation"));
3436 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3437 }
3438
3439 #[test]
3440 fn test_capability_registry_get() {
3441 let registry = CapabilityRegistry::with_builtins();
3442
3443 let noop = registry.get("noop").unwrap();
3444 assert_eq!(noop.id(), "noop");
3445 assert_eq!(noop.name(), "No-Op");
3446 assert_eq!(noop.status(), CapabilityStatus::Available);
3447 }
3448
3449 #[test]
3457 fn builtin_capabilities_satisfy_registry_invariants() {
3458 let registry = CapabilityRegistry::with_builtins();
3459
3460 for cap in registry.list() {
3461 let id = cap.id();
3462 assert!(!id.is_empty(), "capability has an empty id");
3463 assert!(
3464 !cap.name().trim().is_empty(),
3465 "capability `{id}` has an empty name"
3466 );
3467
3468 assert!(
3471 registry.get(id).is_some(),
3472 "capability `{id}` does not resolve by its own id"
3473 );
3474
3475 for dep in cap.dependencies() {
3479 assert!(
3480 registry.get(dep).is_some(),
3481 "capability `{id}` depends on `{dep}`, which is not registered"
3482 );
3483 }
3484
3485 let mut seen = std::collections::HashSet::new();
3488 for tool in cap.tools() {
3489 let name = tool.name().to_string();
3490 assert!(
3491 !name.is_empty(),
3492 "capability `{id}` exposes a tool with an empty name"
3493 );
3494 assert!(
3495 seen.insert(name.clone()),
3496 "capability `{id}` exposes duplicate tool name `{name}`"
3497 );
3498 }
3499
3500 let mut def_seen = std::collections::HashSet::new();
3503 for def in cap.tool_definitions() {
3504 let name = def.name().to_string();
3505 assert!(
3506 !name.is_empty(),
3507 "capability `{id}` advertises a tool definition with an empty name"
3508 );
3509 assert!(
3510 def_seen.insert(name.clone()),
3511 "capability `{id}` advertises duplicate tool definition name `{name}`"
3512 );
3513 }
3514 }
3515 }
3516
3517 #[test]
3529 fn builtin_tools_have_narration_or_documented_generic_fallback() {
3530 use crate::tool_narration::{ToolNarrationContext, ToolNarrationPhase};
3531 use crate::tool_types::ToolCall;
3532
3533 const GENERIC_NARRATION_ALLOWLIST: &[(&str, &str)] = &[
3536 ("sample_data", "demo capability with fixture mounts"),
3538 (
3539 "data_knowledge",
3540 "demo knowledge scaffold; fixture data only",
3541 ),
3542 ("fake_aws", "demo/eval fixture tools"),
3543 ("fake_crm", "demo/eval fixture tools"),
3544 ("fake_financial", "demo/eval fixture tools"),
3545 ("fake_warehouse", "demo/eval fixture tools"),
3546 ("test_math", "test fixture capability"),
3547 ("test_weather", "test fixture capability"),
3548 (
3553 "platform_management",
3554 "operator admin surface; mutations narrate via narration_noun, reads use display names",
3555 ),
3556 (
3559 "model_scout",
3560 "operator model-routing tools; display-name presentation is adequate",
3561 ),
3562 (
3563 "openrouter_workspace",
3564 "operator OpenRouter inspection tools; display-name presentation is adequate",
3565 ),
3566 (
3569 "lua",
3570 "arbitrary sandboxed code execution; display-name presentation is adequate",
3571 ),
3572 ];
3573
3574 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3577 let ctx = ToolNarrationContext::default();
3578 let mut missing: Vec<String> = Vec::new();
3579
3580 for cap in registry.list() {
3581 let cap_id = cap.id().to_string();
3582 if GENERIC_NARRATION_ALLOWLIST
3583 .iter()
3584 .any(|(id, _)| *id == cap_id)
3585 {
3586 continue;
3587 }
3588
3589 for tool in cap.tools() {
3590 let def = tool.to_definition();
3591 if def.hints().narration_noun.is_some() {
3594 continue;
3595 }
3596
3597 let call = ToolCall {
3598 id: "call_narration_audit".to_string(),
3599 name: tool.name().to_string(),
3600 arguments: serde_json::json!({}),
3601 };
3602 if cap
3605 .narrate(Some(&def), &call, ToolNarrationPhase::Started, None, ctx)
3606 .is_none()
3607 {
3608 missing.push(format!("{cap_id}::{}", tool.name()));
3609 }
3610 }
3611 }
3612
3613 assert!(
3614 missing.is_empty(),
3615 "These built-in tools fall back to raw tool-call presentation. Implement \
3616 `Tool::narrate` (see specs/tool-narration.md), set a `narration_noun` hint, \
3617 or add a documented entry to GENERIC_NARRATION_ALLOWLIST: {missing:?}"
3618 );
3619 }
3620
3621 #[test]
3622 fn test_capability_registry_blueprint_with_capability() {
3623 struct BlueprintProviderCapability;
3624
3625 impl Capability for BlueprintProviderCapability {
3626 fn id(&self) -> &str {
3627 "blueprint_provider"
3628 }
3629 fn name(&self) -> &str {
3630 "Blueprint Provider"
3631 }
3632 fn description(&self) -> &str {
3633 "Capability that provides a blueprint for tests"
3634 }
3635 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3636 vec![AgentBlueprint {
3637 id: "test_blueprint",
3638 name: "Test Blueprint",
3639 description: "Blueprint for capability registry tests",
3640 model: BlueprintModel::Inherit,
3641 system_prompt: "Test prompt",
3642 tools: vec![],
3643 max_turns: None,
3644 config_schema: None,
3645 }]
3646 }
3647 }
3648
3649 let mut registry = CapabilityRegistry::new();
3650 registry.register(BlueprintProviderCapability);
3651
3652 let (capability_id, blueprint) = registry
3653 .blueprint_with_capability("test_blueprint")
3654 .expect("blueprint should resolve with capability id");
3655 assert_eq!(capability_id, "blueprint_provider");
3656 assert_eq!(blueprint.id, "test_blueprint");
3657 }
3658
3659 #[test]
3660 fn test_capability_registry_builder() {
3661 let registry = CapabilityRegistry::builder()
3662 .capability(NoopCapability)
3663 .capability(CurrentTimeCapability)
3664 .build();
3665
3666 assert!(registry.has("noop"));
3667 assert!(registry.has("current_time"));
3668 assert_eq!(registry.len(), 2);
3669 }
3670
3671 #[test]
3672 fn test_capability_status() {
3673 let registry = CapabilityRegistry::with_builtins();
3674
3675 let current_time = registry.get("current_time").unwrap();
3676 assert_eq!(current_time.status(), CapabilityStatus::Available);
3677
3678 let research = registry.get("research").unwrap();
3679 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3680 }
3681
3682 #[test]
3683 fn test_capability_icons_and_categories() {
3684 let registry = CapabilityRegistry::with_builtins();
3685
3686 let noop = registry.get("noop").unwrap();
3687 assert_eq!(noop.icon(), Some("circle-off"));
3688 assert_eq!(noop.category(), Some("Testing"));
3689
3690 let current_time = registry.get("current_time").unwrap();
3691 assert_eq!(current_time.icon(), Some("clock"));
3692 assert_eq!(current_time.category(), Some("Core"));
3693 }
3694
3695 #[test]
3696 fn test_system_prompt_preview_default_delegates_to_addition() {
3697 let registry = CapabilityRegistry::with_builtins();
3698
3699 let test_math = registry.get("test_math").unwrap();
3701 assert_eq!(
3702 test_math.system_prompt_preview().as_deref(),
3703 test_math.system_prompt_addition()
3704 );
3705
3706 let current_time = registry.get("current_time").unwrap();
3708 assert!(current_time.system_prompt_preview().is_none());
3709 assert!(current_time.system_prompt_addition().is_none());
3710 }
3711
3712 #[test]
3713 fn test_system_prompt_preview_dynamic_capability() {
3714 let registry = CapabilityRegistry::with_builtins();
3715 let cap = registry.get("agent_instructions").unwrap();
3716
3717 assert!(cap.system_prompt_addition().is_none());
3719 assert!(cap.system_prompt_preview().is_some());
3720 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3721 }
3722
3723 #[tokio::test]
3728 async fn test_apply_capabilities_empty() {
3729 let registry = CapabilityRegistry::with_builtins();
3730 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3731
3732 let applied =
3733 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3734
3735 assert_eq!(
3736 applied.runtime_agent.system_prompt,
3737 base_runtime_agent.system_prompt
3738 );
3739 assert!(applied.tool_registry.is_empty());
3740 assert!(applied.applied_ids.is_empty());
3741 }
3742
3743 #[tokio::test]
3744 async fn test_apply_capabilities_noop() {
3745 let registry = CapabilityRegistry::with_builtins();
3746 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3747
3748 let applied = apply_capabilities(
3749 base_runtime_agent.clone(),
3750 &["noop".to_string()],
3751 ®istry,
3752 &test_ctx(),
3753 )
3754 .await;
3755
3756 assert_eq!(
3758 applied.runtime_agent.system_prompt,
3759 base_runtime_agent.system_prompt
3760 );
3761 assert!(applied.tool_registry.is_empty());
3762 assert_eq!(applied.applied_ids, vec!["noop"]);
3763 }
3764
3765 #[tokio::test]
3766 async fn test_apply_capabilities_current_time() {
3767 let registry = CapabilityRegistry::with_builtins();
3768 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3769
3770 let applied = apply_capabilities(
3771 base_runtime_agent.clone(),
3772 &["current_time".to_string()],
3773 ®istry,
3774 &test_ctx(),
3775 )
3776 .await;
3777
3778 assert!(
3782 applied
3783 .runtime_agent
3784 .system_prompt
3785 .contains(FACTS_DYNAMIC_NOTE),
3786 "current_time should contribute the dynamic-facts note"
3787 );
3788 assert!(
3789 applied
3790 .runtime_agent
3791 .system_prompt
3792 .contains(&base_runtime_agent.system_prompt),
3793 "base prompt is preserved"
3794 );
3795 assert!(applied.tool_registry.has("get_current_time"));
3796 assert_eq!(applied.tool_registry.len(), 1);
3797 assert_eq!(applied.applied_ids, vec!["current_time"]);
3798 }
3799
3800 #[tokio::test]
3801 async fn test_apply_capabilities_skips_coming_soon() {
3802 let registry = CapabilityRegistry::with_builtins();
3803 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3804
3805 let applied = apply_capabilities(
3807 base_runtime_agent.clone(),
3808 &["research".to_string()],
3809 ®istry,
3810 &test_ctx(),
3811 )
3812 .await;
3813
3814 assert_eq!(
3816 applied.runtime_agent.system_prompt,
3817 base_runtime_agent.system_prompt
3818 );
3819 assert!(applied.applied_ids.is_empty()); }
3821
3822 #[tokio::test]
3823 async fn test_apply_capabilities_multiple() {
3824 let registry = CapabilityRegistry::with_builtins();
3825 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3826
3827 let applied = apply_capabilities(
3828 base_runtime_agent.clone(),
3829 &["noop".to_string(), "current_time".to_string()],
3830 ®istry,
3831 &test_ctx(),
3832 )
3833 .await;
3834
3835 assert!(applied.tool_registry.has("get_current_time"));
3836 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3837 }
3838
3839 #[tokio::test]
3840 async fn test_apply_capabilities_preserves_order() {
3841 let registry = CapabilityRegistry::with_builtins();
3842 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3843
3844 let applied = apply_capabilities(
3846 base_runtime_agent,
3847 &["current_time".to_string(), "noop".to_string()],
3848 ®istry,
3849 &test_ctx(),
3850 )
3851 .await;
3852
3853 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3854 }
3855
3856 #[tokio::test]
3857 async fn test_apply_capabilities_test_math() {
3858 let registry = CapabilityRegistry::with_builtins();
3859 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3860
3861 let applied = apply_capabilities(
3862 base_runtime_agent.clone(),
3863 &["test_math".to_string()],
3864 ®istry,
3865 &test_ctx(),
3866 )
3867 .await;
3868
3869 assert!(
3871 !applied
3872 .runtime_agent
3873 .system_prompt
3874 .contains("<capability id=\"test_math\">")
3875 );
3876 assert!(
3878 applied
3879 .runtime_agent
3880 .system_prompt
3881 .contains("You are a helpful assistant.")
3882 );
3883 assert!(applied.tool_registry.has("add"));
3884 assert!(applied.tool_registry.has("subtract"));
3885 assert!(applied.tool_registry.has("multiply"));
3886 assert!(applied.tool_registry.has("divide"));
3887 assert_eq!(applied.tool_registry.len(), 4);
3888 }
3889
3890 #[tokio::test]
3891 async fn test_apply_capabilities_test_weather() {
3892 let registry = CapabilityRegistry::with_builtins();
3893 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3894
3895 let applied = apply_capabilities(
3896 base_runtime_agent.clone(),
3897 &["test_weather".to_string()],
3898 ®istry,
3899 &test_ctx(),
3900 )
3901 .await;
3902
3903 assert!(
3905 !applied
3906 .runtime_agent
3907 .system_prompt
3908 .contains("<capability id=\"test_weather\">")
3909 );
3910 assert!(applied.tool_registry.has("get_weather"));
3911 assert!(applied.tool_registry.has("get_forecast"));
3912 assert_eq!(applied.tool_registry.len(), 2);
3913 }
3914
3915 #[tokio::test]
3916 async fn test_apply_capabilities_test_math_and_test_weather() {
3917 let registry = CapabilityRegistry::with_builtins();
3918 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3919
3920 let applied = apply_capabilities(
3921 base_runtime_agent.clone(),
3922 &["test_math".to_string(), "test_weather".to_string()],
3923 ®istry,
3924 &test_ctx(),
3925 )
3926 .await;
3927
3928 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3931 assert!(applied.tool_registry.has("get_weather"));
3932 }
3933
3934 #[tokio::test]
3935 async fn test_apply_capabilities_stateless_todo_list() {
3936 let registry = CapabilityRegistry::with_builtins();
3937 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3938
3939 let applied = apply_capabilities(
3940 base_runtime_agent.clone(),
3941 &["stateless_todo_list".to_string()],
3942 ®istry,
3943 &test_ctx(),
3944 )
3945 .await;
3946
3947 assert!(
3949 applied
3950 .runtime_agent
3951 .system_prompt
3952 .contains("Task Management")
3953 );
3954 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3955 assert!(applied.tool_registry.has("write_todos"));
3956 assert_eq!(applied.tool_registry.len(), 1);
3957 }
3958
3959 #[tokio::test]
3960 async fn test_apply_capabilities_web_fetch() {
3961 let registry = CapabilityRegistry::with_builtins();
3962 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3963
3964 let applied = apply_capabilities(
3965 base_runtime_agent.clone(),
3966 &["web_fetch".to_string()],
3967 ®istry,
3968 &test_ctx(),
3969 )
3970 .await;
3971
3972 assert!(
3974 applied
3975 .runtime_agent
3976 .system_prompt
3977 .contains(&base_runtime_agent.system_prompt)
3978 );
3979 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3980 assert!(applied.tool_registry.has("web_fetch"));
3981 assert_eq!(applied.tool_registry.len(), 1);
3982 }
3983
3984 #[tokio::test]
3989 async fn test_xml_tags_wrap_capability_prompts() {
3990 let registry = CapabilityRegistry::with_builtins();
3991 let collected =
3992 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3993 .await;
3994
3995 assert_eq!(collected.system_prompt_parts.len(), 1);
3996 let part = &collected.system_prompt_parts[0];
3997 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3998 assert!(part.ends_with("</capability>"));
3999 assert!(part.contains("Task Management"));
4000 }
4001
4002 #[tokio::test]
4003 async fn test_xml_tags_multiple_capabilities() {
4004 let registry = CapabilityRegistry::with_builtins();
4005 let collected = collect_capabilities(
4006 &[
4007 "stateless_todo_list".to_string(),
4008 "session_schedule".to_string(),
4009 ],
4010 ®istry,
4011 &test_ctx(),
4012 )
4013 .await;
4014
4015 assert_eq!(collected.system_prompt_parts.len(), 2);
4016 assert!(
4017 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
4018 );
4019 assert!(
4020 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
4021 );
4022
4023 let prefix = collected.system_prompt_prefix().unwrap();
4024 assert!(prefix.contains("</capability>\n\n<capability"));
4026 }
4027
4028 #[tokio::test]
4029 async fn test_xml_tags_system_prompt_wrapping() {
4030 let registry = CapabilityRegistry::with_builtins();
4031 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4032
4033 let applied = apply_capabilities(
4034 base,
4035 &["stateless_todo_list".to_string()],
4036 ®istry,
4037 &test_ctx(),
4038 )
4039 .await;
4040
4041 let prompt = &applied.runtime_agent.system_prompt;
4042 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4043 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
4045 assert!(prompt.contains("</capability>"));
4046 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4048 }
4049
4050 #[tokio::test]
4051 async fn test_no_xml_wrapping_without_capabilities() {
4052 let registry = CapabilityRegistry::with_builtins();
4053 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4054
4055 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
4056
4057 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4059 assert!(
4060 !applied
4061 .runtime_agent
4062 .system_prompt
4063 .contains("<system-prompt>")
4064 );
4065 }
4066
4067 #[tokio::test]
4068 async fn test_no_xml_wrapping_for_noop_capability() {
4069 let registry = CapabilityRegistry::with_builtins();
4070 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4071
4072 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
4074
4075 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4076 assert!(
4077 !applied
4078 .runtime_agent
4079 .system_prompt
4080 .contains("<system-prompt>")
4081 );
4082 }
4083
4084 #[tokio::test]
4089 async fn test_collect_capabilities_includes_mounts() {
4090 let registry = CapabilityRegistry::with_builtins();
4091
4092 let collected =
4093 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4094
4095 assert!(!collected.mounts.is_empty());
4096 assert_eq!(collected.mounts.len(), 1);
4097 assert_eq!(collected.mounts[0].path, "/samples");
4098 assert!(collected.mounts[0].is_readonly());
4099 }
4100
4101 #[tokio::test]
4102 async fn test_collect_capabilities_empty_mounts_by_default() {
4103 let registry = CapabilityRegistry::with_builtins();
4104
4105 let collected =
4107 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4108
4109 assert!(collected.mounts.is_empty());
4110 }
4111
4112 #[tokio::test]
4113 async fn test_dynamic_facts_add_note_without_static_block() {
4114 let registry = CapabilityRegistry::with_builtins();
4118 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4119 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4120 let prompt = collected.system_prompt_parts.join("\n");
4121 assert!(
4122 prompt.contains(FACTS_DYNAMIC_NOTE),
4123 "dynamic-facts note should be in the cached prompt"
4124 );
4125 assert!(
4126 !prompt.contains("<facts>\n"),
4127 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
4128 );
4129 }
4130
4131 #[tokio::test]
4132 async fn test_static_facts_fold_into_prompt() {
4133 struct StaticFactCap;
4134 impl Capability for StaticFactCap {
4135 fn id(&self) -> &str {
4136 "test_static_fact"
4137 }
4138 fn name(&self) -> &str {
4139 "Static Fact"
4140 }
4141 fn description(&self) -> &str {
4142 "test"
4143 }
4144 fn status(&self) -> CapabilityStatus {
4145 CapabilityStatus::Available
4146 }
4147 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
4148 vec![Fact::stat("workspace_root", "/workspace")]
4149 }
4150 }
4151 let mut registry = CapabilityRegistry::new();
4152 registry.register(StaticFactCap);
4153 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
4154 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4155 let prompt = collected.system_prompt_parts.join("\n");
4156 assert!(
4157 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
4158 "static fact should fold into the cached prompt; got: {prompt}"
4159 );
4160 assert!(
4161 !prompt.contains(FACTS_DYNAMIC_NOTE),
4162 "no dynamic note when only static facts exist"
4163 );
4164 }
4165
4166 #[test]
4167 fn test_collect_dynamic_facts_returns_current_time() {
4168 let registry = CapabilityRegistry::with_builtins();
4169 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4170 let facts = collect_dynamic_facts(
4171 &configs,
4172 ®istry,
4173 None,
4174 &FactsContext::new(SessionId::new()),
4175 );
4176 assert_eq!(facts.len(), 1);
4177 assert_eq!(facts[0].key, "current_time");
4178 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4179 }
4180
4181 #[tokio::test]
4182 async fn test_collect_capabilities_combines_mounts() {
4183 let registry = CapabilityRegistry::with_builtins();
4184
4185 let collected = collect_capabilities(
4188 &["sample_data".to_string(), "current_time".to_string()],
4189 ®istry,
4190 &test_ctx(),
4191 )
4192 .await;
4193
4194 assert_eq!(collected.mounts.len(), 1);
4195 assert!(
4197 collected
4198 .applied_ids
4199 .iter()
4200 .any(|id| id == "session_file_system")
4201 );
4202 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4203 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4204 }
4205
4206 #[test]
4207 fn test_sample_data_capability() {
4208 let registry = CapabilityRegistry::with_builtins();
4209 let cap = registry.get("sample_data").unwrap();
4210
4211 assert_eq!(cap.id(), "sample_data");
4212 assert_eq!(cap.name(), "Sample Data");
4213 assert_eq!(cap.status(), CapabilityStatus::Available);
4214
4215 assert!(cap.system_prompt_addition().is_some());
4217 assert!(cap.tools().is_empty());
4218
4219 assert!(!cap.mounts().is_empty());
4221 }
4222
4223 #[test]
4228 fn test_resolve_dependencies_empty() {
4229 let registry = CapabilityRegistry::with_builtins();
4230
4231 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4232
4233 assert!(resolved.resolved_ids.is_empty());
4234 assert!(resolved.added_as_dependencies.is_empty());
4235 assert!(resolved.user_selected.is_empty());
4236 }
4237
4238 #[test]
4239 fn test_resolve_dependencies_no_deps() {
4240 let registry = CapabilityRegistry::with_builtins();
4241
4242 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4244
4245 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4246 assert!(resolved.added_as_dependencies.is_empty());
4247 }
4248
4249 #[test]
4250 fn test_resolve_dependencies_with_deps() {
4251 let registry = CapabilityRegistry::with_builtins();
4252
4253 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4255
4256 assert_eq!(resolved.resolved_ids.len(), 2);
4258 let fs_pos = resolved
4259 .resolved_ids
4260 .iter()
4261 .position(|id| id == "session_file_system")
4262 .unwrap();
4263 let sd_pos = resolved
4264 .resolved_ids
4265 .iter()
4266 .position(|id| id == "sample_data")
4267 .unwrap();
4268 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4269
4270 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4272 }
4273
4274 #[test]
4275 fn test_resolve_dependencies_already_selected() {
4276 let registry = CapabilityRegistry::with_builtins();
4277
4278 let resolved = resolve_dependencies(
4280 &["session_file_system".to_string(), "sample_data".to_string()],
4281 ®istry,
4282 )
4283 .unwrap();
4284
4285 assert_eq!(resolved.resolved_ids.len(), 2);
4286 assert!(resolved.added_as_dependencies.is_empty());
4288 }
4289
4290 #[test]
4291 fn test_resolve_dependencies_preserves_order() {
4292 let registry = CapabilityRegistry::with_builtins();
4293
4294 let resolved =
4296 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4297 .unwrap();
4298
4299 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4300 }
4301
4302 #[test]
4303 fn test_resolve_dependencies_unknown_capability() {
4304 let registry = CapabilityRegistry::with_builtins();
4305
4306 let resolved =
4308 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4309
4310 assert!(resolved.resolved_ids.is_empty());
4311 }
4312
4313 #[test]
4314 fn test_get_dependencies() {
4315 let registry = CapabilityRegistry::with_builtins();
4316
4317 let deps = get_dependencies("sample_data", ®istry);
4319 assert_eq!(deps, vec!["session_file_system"]);
4320
4321 let deps = get_dependencies("current_time", ®istry);
4323 assert!(deps.is_empty());
4324
4325 let deps = get_dependencies("unknown", ®istry);
4327 assert!(deps.is_empty());
4328 }
4329
4330 #[test]
4331 fn test_sample_data_has_dependency() {
4332 let registry = CapabilityRegistry::with_builtins();
4333 let cap = registry.get("sample_data").unwrap();
4334
4335 let deps = cap.dependencies();
4336 assert_eq!(deps.len(), 1);
4337 assert_eq!(deps[0], "session_file_system");
4338 }
4339
4340 #[test]
4341 fn test_noop_has_no_dependencies() {
4342 let registry = CapabilityRegistry::with_builtins();
4343 let cap = registry.get("noop").unwrap();
4344
4345 assert!(cap.dependencies().is_empty());
4346 }
4347
4348 #[test]
4352 fn test_circular_dependency_error() {
4353 struct CapA;
4355 struct CapB;
4356
4357 impl Capability for CapA {
4358 fn id(&self) -> &str {
4359 "test_cap_a"
4360 }
4361 fn name(&self) -> &str {
4362 "Test A"
4363 }
4364 fn description(&self) -> &str {
4365 "Test capability A"
4366 }
4367 fn dependencies(&self) -> Vec<&'static str> {
4368 vec!["test_cap_b"]
4369 }
4370 }
4371
4372 impl Capability for CapB {
4373 fn id(&self) -> &str {
4374 "test_cap_b"
4375 }
4376 fn name(&self) -> &str {
4377 "Test B"
4378 }
4379 fn description(&self) -> &str {
4380 "Test capability B"
4381 }
4382 fn dependencies(&self) -> Vec<&'static str> {
4383 vec!["test_cap_a"]
4384 }
4385 }
4386
4387 let mut registry = CapabilityRegistry::new();
4388 registry.register(CapA);
4389 registry.register(CapB);
4390
4391 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4392
4393 assert!(result.is_err());
4394 match result.unwrap_err() {
4395 DependencyError::CircularDependency { capability_id, .. } => {
4396 assert_eq!(capability_id, "test_cap_a");
4397 }
4398 _ => panic!("Expected CircularDependency error"),
4399 }
4400 }
4401
4402 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4407
4408 struct FilterTestCapability {
4410 priority: i32,
4411 }
4412
4413 impl Capability for FilterTestCapability {
4414 fn id(&self) -> &str {
4415 "filter_test"
4416 }
4417 fn name(&self) -> &str {
4418 "Filter Test"
4419 }
4420 fn description(&self) -> &str {
4421 "Test capability with message filter"
4422 }
4423 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4424 Some(Arc::new(FilterTestProvider {
4425 priority: self.priority,
4426 }))
4427 }
4428 }
4429
4430 struct FilterTestProvider {
4431 priority: i32,
4432 }
4433
4434 impl MessageFilterProvider for FilterTestProvider {
4435 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4436 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4438 query
4439 .filters
4440 .push(MessageFilter::Search(search.to_string()));
4441 }
4442 }
4443
4444 fn priority(&self) -> i32 {
4445 self.priority
4446 }
4447 }
4448
4449 #[tokio::test]
4450 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4451 let registry = CapabilityRegistry::with_builtins();
4452 let configs = vec![AgentCapabilityConfig {
4453 capability_ref: CapabilityId::new("current_time"),
4454 config: serde_json::json!({}),
4455 }];
4456
4457 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4458
4459 assert!(collected.message_filter_providers.is_empty());
4460 assert!(!collected.has_message_filters());
4461 }
4462
4463 #[tokio::test]
4464 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4465 let mut registry = CapabilityRegistry::new();
4466 registry.register(FilterTestCapability { priority: 0 });
4467
4468 let configs = vec![AgentCapabilityConfig {
4469 capability_ref: CapabilityId::new("filter_test"),
4470 config: serde_json::json!({ "search": "hello" }),
4471 }];
4472
4473 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4474
4475 assert_eq!(collected.message_filter_providers.len(), 1);
4476 assert!(collected.has_message_filters());
4477 }
4478
4479 #[tokio::test]
4480 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4481 struct HighPriorityCapability;
4483 struct LowPriorityCapability;
4484
4485 impl Capability for HighPriorityCapability {
4486 fn id(&self) -> &str {
4487 "high_priority"
4488 }
4489 fn name(&self) -> &str {
4490 "High Priority"
4491 }
4492 fn description(&self) -> &str {
4493 "Test"
4494 }
4495 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4496 Some(Arc::new(FilterTestProvider { priority: 10 }))
4497 }
4498 }
4499
4500 impl Capability for LowPriorityCapability {
4501 fn id(&self) -> &str {
4502 "low_priority"
4503 }
4504 fn name(&self) -> &str {
4505 "Low Priority"
4506 }
4507 fn description(&self) -> &str {
4508 "Test"
4509 }
4510 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4511 Some(Arc::new(FilterTestProvider { priority: -5 }))
4512 }
4513 }
4514
4515 let mut registry = CapabilityRegistry::new();
4516 registry.register(HighPriorityCapability);
4517 registry.register(LowPriorityCapability);
4518
4519 let configs = vec![
4521 AgentCapabilityConfig {
4522 capability_ref: CapabilityId::new("high_priority"),
4523 config: serde_json::json!({}),
4524 },
4525 AgentCapabilityConfig {
4526 capability_ref: CapabilityId::new("low_priority"),
4527 config: serde_json::json!({}),
4528 },
4529 ];
4530
4531 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4532
4533 assert_eq!(collected.message_filter_providers.len(), 2);
4535 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4536 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4537 }
4538
4539 #[tokio::test]
4540 async fn test_collected_capabilities_apply_message_filters() {
4541 let mut registry = CapabilityRegistry::new();
4542 registry.register(FilterTestCapability { priority: 0 });
4543
4544 let configs = vec![AgentCapabilityConfig {
4545 capability_ref: CapabilityId::new("filter_test"),
4546 config: serde_json::json!({ "search": "test_query" }),
4547 }];
4548
4549 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4550
4551 let session_id: SessionId = Uuid::now_v7().into();
4553 let mut query = MessageQuery::new(session_id);
4554
4555 collected.apply_message_filters(&mut query);
4556
4557 assert_eq!(query.filters.len(), 1);
4559 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4560 }
4561
4562 #[tokio::test]
4563 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4564 struct SearchCapability {
4565 id: &'static str,
4566 search_term: &'static str,
4567 priority: i32,
4568 }
4569
4570 struct SearchProvider {
4571 search_term: &'static str,
4572 priority: i32,
4573 }
4574
4575 impl MessageFilterProvider for SearchProvider {
4576 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4577 query
4578 .filters
4579 .push(MessageFilter::Search(self.search_term.to_string()));
4580 }
4581
4582 fn priority(&self) -> i32 {
4583 self.priority
4584 }
4585 }
4586
4587 impl Capability for SearchCapability {
4588 fn id(&self) -> &str {
4589 self.id
4590 }
4591 fn name(&self) -> &str {
4592 "Search"
4593 }
4594 fn description(&self) -> &str {
4595 "Test"
4596 }
4597 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4598 Some(Arc::new(SearchProvider {
4599 search_term: self.search_term,
4600 priority: self.priority,
4601 }))
4602 }
4603 }
4604
4605 let mut registry = CapabilityRegistry::new();
4606 registry.register(SearchCapability {
4607 id: "cap_a",
4608 search_term: "alpha",
4609 priority: 5,
4610 });
4611 registry.register(SearchCapability {
4612 id: "cap_b",
4613 search_term: "beta",
4614 priority: 1,
4615 });
4616 registry.register(SearchCapability {
4617 id: "cap_c",
4618 search_term: "gamma",
4619 priority: 10,
4620 });
4621
4622 let configs = vec![
4623 AgentCapabilityConfig {
4624 capability_ref: CapabilityId::new("cap_a"),
4625 config: serde_json::json!({}),
4626 },
4627 AgentCapabilityConfig {
4628 capability_ref: CapabilityId::new("cap_b"),
4629 config: serde_json::json!({}),
4630 },
4631 AgentCapabilityConfig {
4632 capability_ref: CapabilityId::new("cap_c"),
4633 config: serde_json::json!({}),
4634 },
4635 ];
4636
4637 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4638
4639 let session_id: SessionId = Uuid::now_v7().into();
4640 let mut query = MessageQuery::new(session_id);
4641
4642 collected.apply_message_filters(&mut query);
4643
4644 assert_eq!(query.filters.len(), 3);
4646 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4647 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4648 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4649 }
4650
4651 #[test]
4652 fn test_capability_without_message_filter_returns_none() {
4653 let registry = CapabilityRegistry::with_builtins();
4654
4655 let noop = registry.get("noop").unwrap();
4656 assert!(noop.message_filter_provider().is_none());
4657
4658 let current_time = registry.get("current_time").unwrap();
4659 assert!(current_time.message_filter_provider().is_none());
4660 }
4661
4662 #[tokio::test]
4663 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4664 let mut registry = CapabilityRegistry::new();
4665 registry.register(FilterTestCapability { priority: 0 });
4666
4667 let test_config = serde_json::json!({
4668 "search": "custom_search",
4669 "extra_field": 42
4670 });
4671
4672 let configs = vec![AgentCapabilityConfig {
4673 capability_ref: CapabilityId::new("filter_test"),
4674 config: test_config.clone(),
4675 }];
4676
4677 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4678
4679 assert_eq!(collected.message_filter_providers.len(), 1);
4681 let (_, stored_config) = &collected.message_filter_providers[0];
4682 assert_eq!(*stored_config, test_config);
4683 }
4684
4685 #[test]
4690 fn test_collect_message_filters_only_collects_filters() {
4691 let mut registry = CapabilityRegistry::new();
4692 registry.register(FilterTestCapability { priority: 0 });
4693
4694 let configs = vec![AgentCapabilityConfig {
4695 capability_ref: CapabilityId::new("filter_test"),
4696 config: serde_json::json!({ "search": "test_query" }),
4697 }];
4698
4699 let collected = collect_message_filters_only(&configs, ®istry);
4700
4701 let session_id: SessionId = Uuid::now_v7().into();
4702 let mut query = MessageQuery::new(session_id);
4703 collected.apply_message_filters(&mut query);
4704
4705 assert_eq!(query.filters.len(), 1);
4706 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4707 }
4708
4709 #[test]
4710 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4711 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4712
4713 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4715 assert_eq!(with["compaction_active"], serde_json::json!(true));
4716 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4717
4718 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4719 assert!(without.get("compaction_active").is_none());
4720
4721 let other = message_filter_config_for("other", &base, true);
4723 assert!(other.get("compaction_active").is_none());
4724
4725 let null_base = message_filter_config_for(
4727 INFINITY_CONTEXT_CAPABILITY_ID,
4728 &serde_json::Value::Null,
4729 true,
4730 );
4731 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4732 }
4733
4734 #[test]
4735 fn test_infinity_context_defers_to_compaction_end_to_end() {
4736 use crate::message::Message;
4737
4738 let mut registry = CapabilityRegistry::new();
4739 registry.register(InfinityContextCapability);
4740 registry.register(CompactionCapability);
4741
4742 let tight = serde_json::json!({
4743 "context_budget_tokens": 1,
4744 "min_recent_messages": 1
4745 });
4746
4747 let solo = vec![AgentCapabilityConfig {
4749 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4750 config: tight.clone(),
4751 }];
4752 let mut messages = vec![
4753 Message::user("task"),
4754 Message::assistant("old ".repeat(400)),
4755 Message::user("recent"),
4756 ];
4757 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4758 assert!(
4759 messages
4760 .iter()
4761 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4762 "infinity context alone should trim and notice"
4763 );
4764
4765 let both = vec![
4767 AgentCapabilityConfig {
4768 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4769 config: tight,
4770 },
4771 AgentCapabilityConfig {
4772 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4773 config: serde_json::json!({}),
4774 },
4775 ];
4776 let mut messages = vec![
4777 Message::user("task"),
4778 Message::assistant("old ".repeat(400)),
4779 Message::user("recent"),
4780 ];
4781 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4782 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4783 assert!(
4784 messages
4785 .iter()
4786 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4787 "no hidden-history notice when compaction is the active reducer"
4788 );
4789 }
4790
4791 #[test]
4792 fn test_compaction_is_enabled_detects_compaction() {
4793 let mut registry = CapabilityRegistry::new();
4794 registry.register(CompactionCapability);
4795
4796 let with_compaction = vec![AgentCapabilityConfig {
4797 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4798 config: serde_json::json!({}),
4799 }];
4800 assert!(compaction_is_enabled(&with_compaction, ®istry));
4801
4802 let without = vec![AgentCapabilityConfig {
4803 capability_ref: CapabilityId::new("current_time"),
4804 config: serde_json::json!({}),
4805 }];
4806 assert!(!compaction_is_enabled(&without, ®istry));
4807 }
4808
4809 #[test]
4810 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4811 let registry = CapabilityRegistry::new();
4812
4813 let configs = vec![AgentCapabilityConfig {
4814 capability_ref: CapabilityId::new("nonexistent"),
4815 config: serde_json::json!({}),
4816 }];
4817
4818 let collected = collect_message_filters_only(&configs, ®istry);
4819 assert!(collected.message_filter_providers.is_empty());
4820 }
4821
4822 #[test]
4823 fn test_collect_message_filters_only_preserves_priority_order() {
4824 struct PriorityFilterCap {
4825 id: &'static str,
4826 search_term: &'static str,
4827 priority: i32,
4828 }
4829
4830 struct PriorityFilterProvider {
4831 search_term: &'static str,
4832 priority: i32,
4833 }
4834
4835 impl Capability for PriorityFilterCap {
4836 fn id(&self) -> &str {
4837 self.id
4838 }
4839 fn name(&self) -> &str {
4840 self.id
4841 }
4842 fn description(&self) -> &str {
4843 "priority test"
4844 }
4845 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4846 Some(Arc::new(PriorityFilterProvider {
4847 search_term: self.search_term,
4848 priority: self.priority,
4849 }))
4850 }
4851 }
4852
4853 impl MessageFilterProvider for PriorityFilterProvider {
4854 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4855 query
4856 .filters
4857 .push(MessageFilter::Search(self.search_term.to_string()));
4858 }
4859 fn priority(&self) -> i32 {
4860 self.priority
4861 }
4862 }
4863
4864 let mut registry = CapabilityRegistry::new();
4865 registry.register(PriorityFilterCap {
4866 id: "gamma",
4867 search_term: "gamma",
4868 priority: 10,
4869 });
4870 registry.register(PriorityFilterCap {
4871 id: "alpha",
4872 search_term: "alpha",
4873 priority: 5,
4874 });
4875 registry.register(PriorityFilterCap {
4876 id: "beta",
4877 search_term: "beta",
4878 priority: 1,
4879 });
4880
4881 let configs = vec![
4882 AgentCapabilityConfig {
4883 capability_ref: CapabilityId::new("gamma"),
4884 config: serde_json::json!({}),
4885 },
4886 AgentCapabilityConfig {
4887 capability_ref: CapabilityId::new("alpha"),
4888 config: serde_json::json!({}),
4889 },
4890 AgentCapabilityConfig {
4891 capability_ref: CapabilityId::new("beta"),
4892 config: serde_json::json!({}),
4893 },
4894 ];
4895
4896 let collected = collect_message_filters_only(&configs, ®istry);
4897
4898 let session_id: SessionId = Uuid::now_v7().into();
4899 let mut query = MessageQuery::new(session_id);
4900 collected.apply_message_filters(&mut query);
4901
4902 assert_eq!(query.filters.len(), 3);
4904 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4905 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4906 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4907 }
4908
4909 #[test]
4910 fn test_collect_message_filters_only_post_load_invoked() {
4911 use crate::message::Message;
4912
4913 struct PostLoadCap;
4914 struct PostLoadProvider;
4915
4916 impl Capability for PostLoadCap {
4917 fn id(&self) -> &str {
4918 "post_load_test"
4919 }
4920 fn name(&self) -> &str {
4921 "PostLoad Test"
4922 }
4923 fn description(&self) -> &str {
4924 "test"
4925 }
4926 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4927 Some(Arc::new(PostLoadProvider))
4928 }
4929 }
4930
4931 impl MessageFilterProvider for PostLoadProvider {
4932 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4933 fn priority(&self) -> i32 {
4934 0
4935 }
4936 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4937 messages.reverse();
4939 }
4940 }
4941
4942 let mut registry = CapabilityRegistry::new();
4943 registry.register(PostLoadCap);
4944
4945 let configs = vec![AgentCapabilityConfig {
4946 capability_ref: CapabilityId::new("post_load_test"),
4947 config: serde_json::json!({}),
4948 }];
4949
4950 let collected = collect_message_filters_only(&configs, ®istry);
4951
4952 let mut messages = vec![Message::user("first"), Message::user("second")];
4953 collected.apply_post_load_filters(&mut messages);
4954
4955 assert_eq!(messages[0].text(), Some("second"));
4957 assert_eq!(messages[1].text(), Some("first"));
4958 }
4959
4960 #[test]
4961 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4962 use crate::tool_types::ToolCall;
4963
4964 fn tool_heavy_messages() -> Vec<Message> {
4965 let mut messages = vec![Message::user("inspect files repeatedly")];
4966 for index in 0..9 {
4967 let call_id = format!("call_{index}");
4968 messages.push(Message::assistant_with_tools(
4969 "",
4970 vec![ToolCall {
4971 id: call_id.clone(),
4972 name: "read_file".to_string(),
4973 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4974 }],
4975 ));
4976 messages.push(Message::tool_result(
4977 call_id,
4978 Some(serde_json::json!({
4979 "path": "/workspace/src/lib.rs",
4980 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4981 "total_lines": 1000,
4982 "lines_shown": {"start": 1, "end": 1000},
4983 "truncated": false
4984 })),
4985 None,
4986 ));
4987 }
4988 messages
4989 }
4990
4991 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4992 messages[2]
4993 .tool_result_content()
4994 .and_then(|result| result.result.as_ref())
4995 .and_then(|result| result.get("masked"))
4996 .and_then(|masked| masked.as_bool())
4997 .unwrap_or(false)
4998 }
4999
5000 let mut registry = CapabilityRegistry::new();
5001 registry.register(CompactionCapability);
5002 let context = ModelViewContext {
5003 session_id: SessionId::new(),
5004 prior_usage: None,
5005 };
5006
5007 let no_compaction = collect_model_view_providers(&[], ®istry, None);
5008 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
5009 assert!(!first_tool_result_is_masked(&unmasked));
5010
5011 let compaction = collect_model_view_providers(
5012 &[AgentCapabilityConfig {
5013 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
5014 config: serde_json::json!({}),
5015 }],
5016 ®istry,
5017 None,
5018 );
5019 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
5020 assert!(first_tool_result_is_masked(&masked));
5021 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
5022 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
5023 }
5024
5025 struct DelegatingFilterCap {
5028 id: &'static str,
5029 inner: std::sync::Arc<InnerFilterCap>,
5030 }
5031 struct InnerFilterCap;
5032
5033 impl Capability for InnerFilterCap {
5034 fn id(&self) -> &str {
5035 "inner_filter"
5036 }
5037 fn name(&self) -> &str {
5038 "Inner Filter"
5039 }
5040 fn description(&self) -> &str {
5041 "inner"
5042 }
5043 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5044 Some(std::sync::Arc::new(SentinelFilter))
5045 }
5046 }
5047 struct SentinelFilter;
5048 impl MessageFilterProvider for SentinelFilter {
5049 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
5050 }
5051 impl Capability for DelegatingFilterCap {
5052 fn id(&self) -> &str {
5053 self.id
5054 }
5055 fn name(&self) -> &str {
5056 "Delegating Filter"
5057 }
5058 fn description(&self) -> &str {
5059 "delegating"
5060 }
5061 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5062 None }
5064 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5065 Some(&*self.inner)
5066 }
5067 }
5068
5069 #[test]
5070 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
5071 let inner = std::sync::Arc::new(InnerFilterCap);
5072 let outer = DelegatingFilterCap {
5073 id: "delegating_filter",
5074 inner: inner.clone(),
5075 };
5076
5077 let mut registry = CapabilityRegistry::new();
5078 registry.register(outer);
5079
5080 let configs = vec![AgentCapabilityConfig {
5081 capability_ref: CapabilityId::new("delegating_filter"),
5082 config: serde_json::json!({}),
5083 }];
5084
5085 let collected = collect_message_filters_only(&configs, ®istry);
5088 assert_eq!(
5089 collected.message_filter_providers.len(),
5090 1,
5091 "provider from resolved inner capability must be collected"
5092 );
5093 }
5094
5095 struct DelegatingMvpCap {
5096 id: &'static str,
5097 inner: std::sync::Arc<InnerMvpCap>,
5098 }
5099 struct InnerMvpCap;
5100
5101 impl Capability for InnerMvpCap {
5102 fn id(&self) -> &str {
5103 "inner_mvp"
5104 }
5105 fn name(&self) -> &str {
5106 "Inner MVP"
5107 }
5108 fn description(&self) -> &str {
5109 "inner"
5110 }
5111 fn model_view_provider(
5112 &self,
5113 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5114 struct NoopMvp;
5116 impl crate::capabilities::ModelViewProvider for NoopMvp {
5117 fn apply_model_view(
5118 &self,
5119 messages: Vec<Message>,
5120 _config: &serde_json::Value,
5121 _context: &ModelViewContext<'_>,
5122 ) -> Vec<Message> {
5123 messages
5124 }
5125 }
5126 Some(std::sync::Arc::new(NoopMvp))
5127 }
5128 }
5129 impl Capability for DelegatingMvpCap {
5130 fn id(&self) -> &str {
5131 self.id
5132 }
5133 fn name(&self) -> &str {
5134 "Delegating MVP"
5135 }
5136 fn description(&self) -> &str {
5137 "delegating"
5138 }
5139 fn model_view_provider(
5140 &self,
5141 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5142 None }
5144 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5145 Some(&*self.inner)
5146 }
5147 }
5148
5149 #[test]
5150 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
5151 let inner = std::sync::Arc::new(InnerMvpCap);
5152 let outer = DelegatingMvpCap {
5153 id: "delegating_mvp",
5154 inner: inner.clone(),
5155 };
5156
5157 let mut registry = CapabilityRegistry::new();
5158 registry.register(outer);
5159
5160 let configs = vec![AgentCapabilityConfig {
5161 capability_ref: CapabilityId::new("delegating_mvp"),
5162 config: serde_json::json!({}),
5163 }];
5164
5165 let collected = collect_model_view_providers(&configs, ®istry, None);
5168 assert_eq!(
5169 collected.model_view_providers.len(),
5170 1,
5171 "provider from resolved inner capability must be collected"
5172 );
5173 }
5174
5175 #[tokio::test]
5185 async fn test_bashkit_shell_capability_produces_bash_tool() {
5186 let registry = CapabilityRegistry::with_builtins();
5187 let collected =
5188 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5189
5190 let tool_names: Vec<&str> = collected
5191 .tool_definitions
5192 .iter()
5193 .map(|t| t.name())
5194 .collect();
5195 assert!(
5196 tool_names.contains(&"bash"),
5197 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5198 tool_names
5199 );
5200 assert!(
5201 !collected.tools.is_empty(),
5202 "bashkit_shell must provide tool implementations"
5203 );
5204 }
5205
5206 #[tokio::test]
5207 async fn test_generic_harness_capability_set_produces_bash_tool() {
5208 let generic_harness_caps = vec![
5211 "session_file_system".to_string(),
5212 "bashkit_shell".to_string(),
5213 "web_fetch".to_string(),
5214 "session_storage".to_string(),
5215 "session".to_string(),
5216 "agent_instructions".to_string(),
5217 "skills".to_string(),
5218 "infinity_context".to_string(),
5219 "auto_tool_search".to_string(),
5220 ];
5221
5222 let registry = CapabilityRegistry::with_builtins();
5223 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5224
5225 let tool_names: Vec<&str> = collected
5226 .tool_definitions
5227 .iter()
5228 .map(|t| t.name())
5229 .collect();
5230 assert!(
5231 tool_names.contains(&"bash"),
5232 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5233 tool_names
5234 );
5235 }
5236
5237 #[tokio::test]
5238 async fn test_collect_capabilities_tool_count_matches_definitions() {
5239 let registry = CapabilityRegistry::with_builtins();
5242 let collected =
5243 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5244
5245 assert_eq!(
5246 collected.tools.len(),
5247 collected.tool_definitions.len(),
5248 "tool implementations ({}) must match tool definitions ({})",
5249 collected.tools.len(),
5250 collected.tool_definitions.len(),
5251 );
5252 }
5253
5254 #[tokio::test]
5258 async fn test_collect_capabilities_resolves_dependencies() {
5259 let registry = CapabilityRegistry::with_builtins();
5262 let collected =
5263 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5264
5265 assert!(
5267 collected
5268 .applied_ids
5269 .iter()
5270 .any(|id| id == "session_file_system"),
5271 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5272 collected.applied_ids
5273 );
5274
5275 let tool_names: Vec<&str> = collected
5276 .tool_definitions
5277 .iter()
5278 .map(|t| t.name())
5279 .collect();
5280
5281 assert!(
5283 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5284 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5285 tool_names
5286 );
5287
5288 assert_eq!(
5290 collected.tools.len(),
5291 collected.tool_definitions.len(),
5292 "dependency-added tools must have implementations, not just definitions"
5293 );
5294 }
5295
5296 #[test]
5297 fn test_defaults_do_not_include_bash() {
5298 let registry = crate::ToolRegistry::with_defaults();
5301 assert!(
5302 !registry.has("bash"),
5303 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5304 );
5305 }
5306
5307 #[tokio::test]
5314 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5315 let registry = CapabilityRegistry::with_builtins();
5316 let collected =
5317 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5318
5319 let tool_names: Vec<&str> = collected
5320 .tool_definitions
5321 .iter()
5322 .map(|t| t.name())
5323 .collect();
5324 assert!(
5325 tool_names.contains(&"spawn_background"),
5326 "spawn_background must be auto-activated when bashkit_shell (a \
5327 background-capable tool) is in the agent's capability set; got: {:?}",
5328 tool_names
5329 );
5330 assert!(
5331 collected
5332 .applied_ids
5333 .iter()
5334 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5335 "background_execution must be in applied_ids when auto-activated; \
5336 got: {:?}",
5337 collected.applied_ids
5338 );
5339
5340 assert!(
5342 collected
5343 .tools
5344 .iter()
5345 .any(|t| t.name() == "spawn_background"),
5346 "spawn_background tool implementation must be present alongside the \
5347 definition (lockstep contract)"
5348 );
5349 }
5350
5351 #[tokio::test]
5354 async fn test_background_execution_does_not_auto_activate_without_hint() {
5355 let registry = CapabilityRegistry::with_builtins();
5356 let collected =
5358 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5359
5360 let tool_names: Vec<&str> = collected
5361 .tool_definitions
5362 .iter()
5363 .map(|t| t.name())
5364 .collect();
5365 assert!(
5366 !tool_names.contains(&"spawn_background"),
5367 "spawn_background must NOT be activated without a background-capable \
5368 tool; got: {:?}",
5369 tool_names
5370 );
5371 assert!(
5372 !collected
5373 .applied_ids
5374 .iter()
5375 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5376 "background_execution must not appear in applied_ids when no \
5377 background-capable tool is present; got: {:?}",
5378 collected.applied_ids
5379 );
5380 }
5381
5382 #[tokio::test]
5383 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5384 let registry = CapabilityRegistry::with_builtins();
5385 let collected = collect_capabilities(
5386 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5387 ®istry,
5388 &test_ctx(),
5389 )
5390 .await;
5391
5392 assert!(
5393 collected
5394 .tools
5395 .iter()
5396 .any(|tool| tool.name() == "spawn_agent"),
5397 "subagent-only sessions should get the unified spawn_agent adapter"
5398 );
5399 let spawn_agent = collected
5400 .tool_definitions
5401 .iter()
5402 .find(|tool| tool.name() == "spawn_agent")
5403 .expect("spawn_agent definition");
5404 assert_eq!(
5405 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5406 serde_json::json!(["subagent"])
5407 );
5408 assert_eq!(
5409 spawn_agent.concurrency_class(),
5410 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5411 "unified spawn_agent must serialize same-batch spawns before cap checks"
5412 );
5413 }
5414
5415 #[tokio::test]
5416 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5417 let mut registry = CapabilityRegistry::new();
5418 registry.register(AgentHandoffCapability);
5419 let agent_id = crate::typed_id::AgentId::new();
5420 let harness_id = crate::typed_id::HarnessId::new();
5421 let configs = vec![AgentCapabilityConfig {
5422 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5423 config: serde_json::json!({
5424 "targets": [{
5425 "id": "aws_operator",
5426 "name": "AWS Operator",
5427 "agent_id": agent_id,
5428 "harness_id": harness_id
5429 }]
5430 }),
5431 }];
5432 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5433
5434 assert!(
5435 collected
5436 .tools
5437 .iter()
5438 .any(|tool| tool.name() == "spawn_agent"),
5439 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5440 );
5441 let spawn_agent = collected
5442 .tool_definitions
5443 .iter()
5444 .find(|tool| tool.name() == "spawn_agent")
5445 .expect("spawn_agent definition");
5446 assert_eq!(
5447 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5448 serde_json::json!(["agent"])
5449 );
5450 }
5451
5452 #[tokio::test]
5453 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5454 let mut registry = CapabilityRegistry::new();
5455 registry.register(SubagentCapability);
5456 registry.register(AgentHandoffCapability);
5457
5458 let agent_id = crate::typed_id::AgentId::new();
5459 let harness_id = crate::typed_id::HarnessId::new();
5460 let configs = vec![
5461 AgentCapabilityConfig {
5462 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5463 config: serde_json::json!({}),
5464 },
5465 AgentCapabilityConfig {
5466 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5467 config: serde_json::json!({
5468 "targets": [{
5469 "id": "aws_operator",
5470 "name": "AWS Operator",
5471 "agent_id": agent_id,
5472 "harness_id": harness_id
5473 }]
5474 }),
5475 },
5476 ];
5477
5478 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5479 let spawn_agent_defs: Vec<_> = collected
5480 .tool_definitions
5481 .iter()
5482 .filter(|tool| tool.name() == "spawn_agent")
5483 .collect();
5484
5485 assert_eq!(spawn_agent_defs.len(), 1);
5486 let schema = spawn_agent_defs[0].parameters();
5487 assert_eq!(
5488 schema["properties"]["target"]["properties"]["type"]["enum"],
5489 serde_json::json!(["subagent", "agent"])
5490 );
5491 assert!(schema.get("oneOf").is_none());
5494 assert!(schema.get("anyOf").is_none());
5495 assert!(schema.get("allOf").is_none());
5496 assert_eq!(
5497 schema["required"],
5498 serde_json::json!(["name", "instructions", "target"])
5499 );
5500 assert_eq!(
5501 schema["properties"]["target"]["oneOf"],
5502 serde_json::json!([
5503 {
5504 "properties": {"type": {"const": "subagent"}}
5505 },
5506 {
5507 "properties": {"type": {"const": "agent"}},
5508 "required": ["type", "id"]
5509 }
5510 ])
5511 );
5512 }
5513
5514 #[cfg(feature = "a2a")]
5515 #[tokio::test]
5516 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5517 let mut registry = CapabilityRegistry::new();
5518 registry.register(SubagentCapability);
5519 registry.register(A2aAgentDelegationCapability);
5520
5521 let configs = vec![
5522 AgentCapabilityConfig {
5523 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5524 config: serde_json::json!({}),
5525 },
5526 AgentCapabilityConfig {
5527 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5528 config: serde_json::json!({
5529 "agents": [{
5530 "id": "local_app",
5531 "name": "Local App",
5532 "base_url": "https://example.com"
5533 }]
5534 }),
5535 },
5536 ];
5537
5538 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5539 let spawn_agent_defs: Vec<_> = collected
5540 .tool_definitions
5541 .iter()
5542 .filter(|tool| tool.name() == "spawn_agent")
5543 .collect();
5544
5545 assert_eq!(spawn_agent_defs.len(), 1);
5546 assert_eq!(
5547 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5548 serde_json::json!(["subagent", "external_a2a"])
5549 );
5550 assert_eq!(
5551 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5552 serde_json::json!(["background", "foreground"])
5553 );
5554 assert!(
5555 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5556 .as_str()
5557 .expect("mode description")
5558 .contains("wait")
5559 );
5560 let schema = spawn_agent_defs[0].parameters();
5561 assert!(schema.get("oneOf").is_none());
5562 assert_eq!(
5566 schema["required"],
5567 serde_json::json!(["name", "instructions", "target"])
5568 );
5569 assert_eq!(
5570 schema["properties"]["target"]["oneOf"],
5571 serde_json::json!([
5572 {
5573 "properties": {"type": {"const": "subagent"}}
5574 },
5575 {
5576 "properties": {"type": {"const": "external_a2a"}},
5577 "anyOf": [
5578 {"required": ["id"]},
5579 {"required": ["external_agent_id"]}
5580 ]
5581 }
5582 ])
5583 );
5584 }
5585
5586 struct ExistingSpawnAgentCapability;
5587
5588 impl Capability for ExistingSpawnAgentCapability {
5589 fn id(&self) -> &str {
5590 "existing_spawn_agent"
5591 }
5592
5593 fn name(&self) -> &str {
5594 "Existing Spawn Agent"
5595 }
5596
5597 fn description(&self) -> &str {
5598 "Test capability that already owns spawn_agent"
5599 }
5600
5601 fn tools(&self) -> Vec<Box<dyn Tool>> {
5602 vec![Box::new(ExistingSpawnAgentTool)]
5603 }
5604 }
5605
5606 struct ExistingSpawnAgentTool;
5607
5608 #[async_trait]
5609 impl Tool for ExistingSpawnAgentTool {
5610 fn name(&self) -> &str {
5611 "spawn_agent"
5612 }
5613
5614 fn description(&self) -> &str {
5615 "Existing spawn_agent test tool"
5616 }
5617
5618 fn parameters_schema(&self) -> serde_json::Value {
5619 serde_json::json!({
5620 "type": "object",
5621 "properties": {
5622 "target": {
5623 "type": "object",
5624 "properties": {
5625 "type": {"type": "string", "enum": ["external_a2a"]}
5626 },
5627 "required": ["type"]
5628 }
5629 },
5630 "required": ["target"]
5631 })
5632 }
5633
5634 async fn execute(
5635 &self,
5636 _arguments: serde_json::Value,
5637 ) -> crate::tools::ToolExecutionResult {
5638 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5639 }
5640 }
5641
5642 #[tokio::test]
5643 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5644 let mut registry = CapabilityRegistry::new();
5645 registry.register(SubagentCapability);
5646 registry.register(ExistingSpawnAgentCapability);
5647
5648 let collected = collect_capabilities(
5649 &[
5650 SUBAGENTS_CAPABILITY_ID.to_string(),
5651 "existing_spawn_agent".to_string(),
5652 ],
5653 ®istry,
5654 &test_ctx(),
5655 )
5656 .await;
5657
5658 let spawn_agent_defs: Vec<_> = collected
5659 .tool_definitions
5660 .iter()
5661 .filter(|tool| tool.name() == "spawn_agent")
5662 .collect();
5663 assert_eq!(spawn_agent_defs.len(), 1);
5664 assert_eq!(
5665 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5666 serde_json::json!(["external_a2a"])
5667 );
5668 }
5669
5670 #[tokio::test]
5671 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5672 let mut registry = CapabilityRegistry::new();
5673 registry.register(AgentHandoffCapability);
5674 registry.register(ExistingSpawnAgentCapability);
5675
5676 let agent_id = crate::typed_id::AgentId::new();
5677 let harness_id = crate::typed_id::HarnessId::new();
5678 let configs = vec![
5679 AgentCapabilityConfig {
5680 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5681 config: serde_json::json!({
5682 "targets": [{
5683 "id": "aws_operator",
5684 "name": "AWS Operator",
5685 "agent_id": agent_id,
5686 "harness_id": harness_id
5687 }]
5688 }),
5689 },
5690 AgentCapabilityConfig {
5691 capability_ref: CapabilityId::new("existing_spawn_agent"),
5692 config: serde_json::json!({}),
5693 },
5694 ];
5695
5696 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5697
5698 let spawn_agent_defs: Vec<_> = collected
5699 .tool_definitions
5700 .iter()
5701 .filter(|tool| tool.name() == "spawn_agent")
5702 .collect();
5703 assert_eq!(spawn_agent_defs.len(), 1);
5704 assert_eq!(
5705 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5706 serde_json::json!(["external_a2a"])
5707 );
5708 }
5709
5710 #[tokio::test]
5714 async fn test_background_execution_explicit_selection_is_idempotent() {
5715 let registry = CapabilityRegistry::with_builtins();
5716 let collected = collect_capabilities(
5717 &[
5718 "bashkit_shell".to_string(),
5719 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5720 ],
5721 ®istry,
5722 &test_ctx(),
5723 )
5724 .await;
5725
5726 let spawn_background_count = collected
5727 .tool_definitions
5728 .iter()
5729 .filter(|t| t.name() == "spawn_background")
5730 .count();
5731 assert_eq!(
5732 spawn_background_count, 1,
5733 "spawn_background must appear exactly once even when \
5734 background_execution is selected explicitly alongside a \
5735 background-capable tool"
5736 );
5737 let applied_count = collected
5738 .applied_ids
5739 .iter()
5740 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5741 .count();
5742 assert_eq!(
5743 applied_count, 1,
5744 "background_execution must appear exactly once in applied_ids"
5745 );
5746 }
5747
5748 #[test]
5753 fn test_defaults_do_not_include_spawn_background() {
5754 let registry = crate::ToolRegistry::with_defaults();
5755 assert!(
5756 !registry.has("spawn_background"),
5757 "with_defaults() must not include 'spawn_background' — it comes \
5758 from the background_execution capability (EVE-501)"
5759 );
5760 }
5761
5762 #[test]
5767 fn test_capability_features_default_empty() {
5768 let registry = CapabilityRegistry::with_builtins();
5769
5770 let noop = registry.get("noop").unwrap();
5772 assert!(noop.features().is_empty());
5773
5774 let current_time = registry.get("current_time").unwrap();
5775 assert!(current_time.features().is_empty());
5776 }
5777
5778 #[test]
5779 fn test_file_system_capability_features() {
5780 let registry = CapabilityRegistry::with_builtins();
5781
5782 let fs = registry.get("session_file_system").unwrap();
5783 assert_eq!(fs.features(), vec!["file_system"]);
5784 }
5785
5786 #[test]
5787 fn test_bashkit_shell_capability_features() {
5788 let registry = CapabilityRegistry::with_builtins();
5789
5790 let bash = registry.get("bashkit_shell").unwrap();
5791 assert_eq!(bash.features(), vec!["file_system"]);
5792 }
5793
5794 #[test]
5795 fn test_alias_resolves_to_canonical_capability() {
5796 let registry = CapabilityRegistry::with_builtins();
5797
5798 let via_alias = registry.get("virtual_bash").unwrap();
5800 assert_eq!(via_alias.id(), "bashkit_shell");
5801 assert!(registry.has("virtual_bash"));
5802 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5803 assert_eq!(
5804 registry.canonical_id("bashkit_shell"),
5805 Some("bashkit_shell")
5806 );
5807 assert_eq!(registry.canonical_id("nonexistent"), None);
5808 }
5809
5810 #[test]
5811 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5812 let registry = CapabilityRegistry::with_builtins();
5813
5814 let resolved = resolve_dependencies(
5817 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5818 ®istry,
5819 )
5820 .unwrap();
5821 let bash_ids: Vec<_> = resolved
5822 .resolved_ids
5823 .iter()
5824 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5825 .collect();
5826 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5827 assert!(
5829 !resolved
5830 .added_as_dependencies
5831 .contains(&"bashkit_shell".to_string())
5832 );
5833 }
5834
5835 #[test]
5836 fn test_alias_preserves_explicit_config_in_resolution() {
5837 let registry = CapabilityRegistry::with_builtins();
5838
5839 let configs = vec![AgentCapabilityConfig::with_config(
5840 "virtual_bash".to_string(),
5841 serde_json::json!({"key": "value"}),
5842 )];
5843 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5844 let bash = resolved
5845 .iter()
5846 .find(|c| c.capability_id() == "bashkit_shell")
5847 .expect("alias must resolve to canonical bashkit_shell config");
5848 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5849 }
5850
5851 #[test]
5852 fn test_unregister_by_alias_removes_capability_and_aliases() {
5853 let mut registry = CapabilityRegistry::with_builtins();
5854
5855 assert!(registry.unregister("virtual_bash").is_some());
5856 assert!(!registry.has("bashkit_shell"));
5857 assert!(!registry.has("virtual_bash"));
5858 }
5859
5860 #[test]
5861 fn test_session_storage_capability_features() {
5862 let registry = CapabilityRegistry::with_builtins();
5863
5864 let storage = registry.get("session_storage").unwrap();
5865 let features = storage.features();
5866 assert!(features.contains(&"secrets"));
5867 assert!(features.contains(&"key_value"));
5868 }
5869
5870 #[test]
5871 fn test_session_schedule_capability_features() {
5872 let registry = CapabilityRegistry::with_builtins();
5873
5874 let schedule = registry.get("session_schedule").unwrap();
5875 assert_eq!(schedule.features(), vec!["schedules"]);
5876 }
5877
5878 #[test]
5879 fn test_session_sql_database_capability_features() {
5880 let registry = CapabilityRegistry::with_builtins();
5881
5882 let sql = registry.get("session_sql_database").unwrap();
5883 assert_eq!(sql.features(), vec!["sql_database"]);
5884 }
5885
5886 #[test]
5887 fn test_sample_data_capability_features() {
5888 let registry = CapabilityRegistry::with_builtins();
5889
5890 let sample = registry.get("sample_data").unwrap();
5891 assert_eq!(sample.features(), vec!["file_system"]);
5892 }
5893
5894 #[test]
5895 fn test_compute_features_empty() {
5896 let registry = CapabilityRegistry::with_builtins();
5897
5898 let features = compute_features(&[], ®istry);
5899 assert!(features.is_empty());
5900 }
5901
5902 #[test]
5903 fn test_compute_features_single_capability() {
5904 let registry = CapabilityRegistry::with_builtins();
5905
5906 let features = compute_features(&["session_schedule".to_string()], ®istry);
5907 assert_eq!(features, vec!["schedules"]);
5908 }
5909
5910 #[test]
5911 fn test_compute_features_multiple_capabilities() {
5912 let registry = CapabilityRegistry::with_builtins();
5913
5914 let features = compute_features(
5915 &[
5916 "session_file_system".to_string(),
5917 "session_storage".to_string(),
5918 "session_schedule".to_string(),
5919 ],
5920 ®istry,
5921 );
5922 assert!(features.contains(&"file_system".to_string()));
5923 assert!(features.contains(&"secrets".to_string()));
5924 assert!(features.contains(&"key_value".to_string()));
5925 assert!(features.contains(&"schedules".to_string()));
5926 }
5927
5928 #[test]
5929 fn test_compute_features_deduplicates() {
5930 let registry = CapabilityRegistry::with_builtins();
5931
5932 let features = compute_features(
5934 &[
5935 "session_file_system".to_string(),
5936 "bashkit_shell".to_string(),
5937 ],
5938 ®istry,
5939 );
5940 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5941 assert_eq!(file_system_count, 1, "file_system should appear only once");
5942 }
5943
5944 #[test]
5945 fn test_compute_features_includes_dependency_features() {
5946 let registry = CapabilityRegistry::with_builtins();
5947
5948 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5950 assert!(features.contains(&"file_system".to_string()));
5951 }
5952
5953 #[test]
5954 fn test_compute_features_generic_harness_set() {
5955 let registry = CapabilityRegistry::with_builtins();
5956
5957 let features = compute_features(
5959 &[
5960 "session_file_system".to_string(),
5961 "bashkit_shell".to_string(),
5962 "session_storage".to_string(),
5963 "session".to_string(),
5964 "session_schedule".to_string(),
5965 ],
5966 ®istry,
5967 );
5968 assert!(features.contains(&"file_system".to_string()));
5969 assert!(features.contains(&"secrets".to_string()));
5970 assert!(features.contains(&"key_value".to_string()));
5971 assert!(features.contains(&"schedules".to_string()));
5972 }
5973
5974 #[test]
5975 fn test_compute_features_unknown_capability_ignored() {
5976 let registry = CapabilityRegistry::with_builtins();
5977
5978 let features = compute_features(
5979 &["unknown_cap".to_string(), "session_schedule".to_string()],
5980 ®istry,
5981 );
5982 assert_eq!(features, vec!["schedules"]);
5983 }
5984
5985 #[test]
5986 fn test_risk_level_ordering() {
5987 assert!(RiskLevel::Low < RiskLevel::Medium);
5988 assert!(RiskLevel::Medium < RiskLevel::High);
5989 }
5990
5991 #[test]
5992 fn test_risk_level_serde_roundtrip() {
5993 let high = RiskLevel::High;
5994 let json = serde_json::to_string(&high).unwrap();
5995 assert_eq!(json, "\"high\"");
5996 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5997 assert_eq!(back, RiskLevel::High);
5998 }
5999
6000 #[test]
6001 fn test_capability_risk_levels() {
6002 let registry = CapabilityRegistry::with_builtins();
6003
6004 let bash = registry.get("bashkit_shell").unwrap();
6006 assert_eq!(bash.risk_level(), RiskLevel::High);
6007
6008 let fetch = registry.get("web_fetch").unwrap();
6010 assert_eq!(fetch.risk_level(), RiskLevel::High);
6011
6012 let noop = registry.get("noop").unwrap();
6014 assert_eq!(noop.risk_level(), RiskLevel::Low);
6015 }
6016
6017 #[tokio::test]
6022 async fn test_apply_capabilities_openai_tool_search() {
6023 let registry = CapabilityRegistry::with_builtins();
6024 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6025
6026 let applied = apply_capabilities(
6027 base_runtime_agent.clone(),
6028 &["openai_tool_search".to_string()],
6029 ®istry,
6030 &test_ctx(),
6031 )
6032 .await;
6033
6034 assert_eq!(
6036 applied.runtime_agent.system_prompt,
6037 base_runtime_agent.system_prompt
6038 );
6039 assert!(applied.tool_registry.is_empty());
6040 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
6041
6042 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6044 assert!(ts.enabled);
6045 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6046 }
6047
6048 #[tokio::test]
6049 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
6050 let registry = CapabilityRegistry::with_builtins();
6051 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6052
6053 let applied = apply_capabilities(
6054 base_runtime_agent,
6055 &[
6056 "current_time".to_string(),
6057 "openai_tool_search".to_string(),
6058 "test_math".to_string(),
6059 ],
6060 ®istry,
6061 &test_ctx(),
6062 )
6063 .await;
6064
6065 assert!(applied.tool_registry.has("get_current_time"));
6067 assert!(applied.tool_registry.has("add"));
6068 assert!(applied.tool_registry.has("subtract"));
6069 assert!(applied.tool_registry.has("multiply"));
6070 assert!(applied.tool_registry.has("divide"));
6071
6072 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6074 assert!(ts.enabled);
6075 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6076 }
6077
6078 #[tokio::test]
6079 async fn test_collect_capabilities_tool_search_custom_threshold() {
6080 let registry = CapabilityRegistry::with_builtins();
6081
6082 let configs = vec![AgentCapabilityConfig {
6083 capability_ref: CapabilityId::new("openai_tool_search"),
6084 config: serde_json::json!({"threshold": 5}),
6085 }];
6086
6087 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6088
6089 let ts = collected.tool_search.as_ref().unwrap();
6090 assert!(ts.enabled);
6091 assert_eq!(ts.threshold, 5);
6092 }
6093
6094 #[tokio::test]
6095 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
6096 let registry = CapabilityRegistry::with_builtins();
6097
6098 let configs = vec![
6099 AgentCapabilityConfig {
6100 capability_ref: CapabilityId::new("auto_tool_search"),
6101 config: serde_json::json!({"threshold": 2}),
6102 },
6103 AgentCapabilityConfig {
6104 capability_ref: CapabilityId::new("test_math"),
6105 config: serde_json::json!({}),
6106 },
6107 ];
6108
6109 let ctx = test_ctx().with_model("claude-3-5-haiku");
6113 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6114
6115 assert!(
6116 collected.tool_search.is_none(),
6117 "auto_tool_search must not set a hosted config on a non-native model"
6118 );
6119 assert!(
6120 collected
6121 .tools
6122 .iter()
6123 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6124 "auto_tool_search must contribute the client-side tool_search tool"
6125 );
6126 assert!(
6127 !collected.tool_definition_hooks.is_empty(),
6128 "auto_tool_search must contribute a client-side deferral hook"
6129 );
6130
6131 let mut transformed = collected.tool_definitions.clone();
6132 for hook in &collected.tool_definition_hooks {
6133 transformed = hook.transform(transformed);
6134 }
6135 let add_tool = transformed
6136 .iter()
6137 .find(|tool| tool.name() == "add")
6138 .expect("test_math contributes add");
6139 assert!(
6140 add_tool.parameters().get("properties").is_none(),
6141 "generic auto_tool_search must honor the configured threshold"
6142 );
6143 }
6144
6145 #[tokio::test]
6146 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
6147 let registry = CapabilityRegistry::with_builtins();
6148
6149 let configs = vec![AgentCapabilityConfig {
6150 capability_ref: CapabilityId::new("auto_tool_search"),
6151 config: serde_json::json!({"threshold": 7}),
6152 }];
6153
6154 let ctx = test_ctx().with_model("gpt-5.4");
6157 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6158
6159 let ts = collected
6160 .tool_search
6161 .as_ref()
6162 .expect("auto_tool_search must set a hosted config on a native model");
6163 assert!(ts.enabled);
6164 assert_eq!(ts.threshold, 7);
6165 assert!(
6166 !collected
6167 .tools
6168 .iter()
6169 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6170 "hosted mechanism must not contribute the client-side tool_search tool"
6171 );
6172 assert!(
6173 collected.tool_definition_hooks.is_empty(),
6174 "hosted mechanism must not contribute a client-side deferral hook"
6175 );
6176 }
6177
6178 #[tokio::test]
6179 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6180 let registry = CapabilityRegistry::with_builtins();
6181
6182 let configs = vec![AgentCapabilityConfig {
6183 capability_ref: CapabilityId::new("auto_tool_search"),
6184 config: serde_json::json!({"threshold": 9}),
6185 }];
6186
6187 let ctx = test_ctx().with_model("claude-opus-4-8");
6190 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6191
6192 let ts = collected
6193 .tool_search
6194 .as_ref()
6195 .expect("auto_tool_search must set a hosted config on a native Claude model");
6196 assert!(ts.enabled);
6197 assert_eq!(ts.threshold, 9);
6198 assert!(
6199 !collected
6200 .tools
6201 .iter()
6202 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6203 "hosted mechanism must not contribute the client-side tool_search tool"
6204 );
6205 assert!(
6206 collected.tool_definition_hooks.is_empty(),
6207 "hosted mechanism must not contribute a client-side deferral hook"
6208 );
6209 }
6210
6211 #[tokio::test]
6212 async fn test_collect_capabilities_no_tool_search_without_capability() {
6213 let registry = CapabilityRegistry::with_builtins();
6214
6215 let configs = vec![AgentCapabilityConfig {
6216 capability_ref: CapabilityId::new("current_time"),
6217 config: serde_json::json!({}),
6218 }];
6219
6220 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6221
6222 assert!(collected.tool_search.is_none());
6223 }
6224
6225 #[tokio::test]
6226 async fn test_collect_capabilities_tool_search_category_propagation() {
6227 let registry = CapabilityRegistry::with_builtins();
6228
6229 let configs = vec![
6231 AgentCapabilityConfig {
6232 capability_ref: CapabilityId::new("test_math"),
6233 config: serde_json::json!({}),
6234 },
6235 AgentCapabilityConfig {
6236 capability_ref: CapabilityId::new("openai_tool_search"),
6237 config: serde_json::json!({}),
6238 },
6239 ];
6240
6241 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6242
6243 assert!(collected.tool_search.is_some());
6245
6246 for tool_def in &collected.tool_definitions {
6248 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6250 assert!(
6251 tool_def.category().is_some(),
6252 "Tool {} should have a category from its capability",
6253 tool_def.name()
6254 );
6255 }
6256 }
6257 }
6258
6259 #[tokio::test]
6260 async fn test_apply_capabilities_prompt_caching() {
6261 let registry = CapabilityRegistry::with_builtins();
6262 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6263
6264 let applied = apply_capabilities(
6265 base_runtime_agent.clone(),
6266 &["prompt_caching".to_string()],
6267 ®istry,
6268 &test_ctx(),
6269 )
6270 .await;
6271
6272 assert_eq!(
6273 applied.runtime_agent.system_prompt,
6274 base_runtime_agent.system_prompt
6275 );
6276 assert!(applied.tool_registry.is_empty());
6277 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6278
6279 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6280 assert!(prompt_cache.enabled);
6281 assert_eq!(
6282 prompt_cache.strategy,
6283 crate::driver_registry::PromptCacheStrategy::Auto
6284 );
6285 assert!(prompt_cache.gemini_cached_content.is_none());
6286 }
6287
6288 #[tokio::test]
6289 async fn test_apply_capabilities_openrouter_server_tools() {
6290 let registry = CapabilityRegistry::with_builtins();
6291 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6292
6293 let configs = vec![AgentCapabilityConfig {
6294 capability_ref: CapabilityId::new("openrouter_server_tools"),
6295 config: serde_json::json!({
6296 "tools": ["web_search", "datetime"],
6297 "web_search_max_results": 4,
6298 }),
6299 }];
6300
6301 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6302 let routing = collected
6303 .openrouter_routing
6304 .as_ref()
6305 .expect("server tools produce routing config");
6306 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6307 assert_eq!(
6308 kinds,
6309 vec![
6310 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6311 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6312 ]
6313 );
6314
6315 let applied = apply_capabilities(
6318 base_runtime_agent,
6319 &["openrouter_server_tools".to_string()],
6320 ®istry,
6321 &test_ctx(),
6322 )
6323 .await;
6324 assert!(applied.tool_registry.is_empty());
6325 assert!(applied.runtime_agent.openrouter_routing.is_none());
6326 }
6327
6328 #[tokio::test]
6329 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6330 let registry = CapabilityRegistry::with_builtins();
6331
6332 let configs = vec![AgentCapabilityConfig {
6333 capability_ref: CapabilityId::new("prompt_caching"),
6334 config: serde_json::json!({"strategy": "auto"}),
6335 }];
6336
6337 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6338
6339 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6340 assert!(prompt_cache.enabled);
6341 assert_eq!(
6342 prompt_cache.strategy,
6343 crate::driver_registry::PromptCacheStrategy::Auto
6344 );
6345 assert!(prompt_cache.gemini_cached_content.is_none());
6346 }
6347
6348 #[tokio::test]
6349 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6350 let registry = CapabilityRegistry::with_builtins();
6351
6352 let configs = vec![AgentCapabilityConfig {
6353 capability_ref: CapabilityId::new("prompt_caching"),
6354 config: serde_json::json!({
6355 "strategy": "auto",
6356 "gemini_cached_content": "cachedContents/demo-cache"
6357 }),
6358 }];
6359
6360 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6361
6362 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6363 assert_eq!(
6364 prompt_cache.gemini_cached_content.as_deref(),
6365 Some("cachedContents/demo-cache")
6366 );
6367 }
6368
6369 #[tokio::test]
6370 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6371 let registry = CapabilityRegistry::with_builtins();
6372
6373 let collected = collect_capabilities_with_configs(
6375 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6376 ®istry,
6377 &test_ctx(),
6378 )
6379 .await;
6380 assert_eq!(collected.parallel_tool_calls, Some(true));
6381
6382 let collected = collect_capabilities_with_configs(
6384 &[AgentCapabilityConfig {
6385 capability_ref: CapabilityId::new("parallel_tool_calls"),
6386 config: serde_json::json!({"mode": "avoid"}),
6387 }],
6388 ®istry,
6389 &test_ctx(),
6390 )
6391 .await;
6392 assert_eq!(collected.parallel_tool_calls, Some(false));
6393
6394 let collected = collect_capabilities_with_configs(
6396 &[AgentCapabilityConfig {
6397 capability_ref: CapabilityId::new("parallel_tool_calls"),
6398 config: serde_json::json!({"mode": "none"}),
6399 }],
6400 ®istry,
6401 &test_ctx(),
6402 )
6403 .await;
6404 assert_eq!(collected.parallel_tool_calls, None);
6405
6406 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6408 assert_eq!(collected.parallel_tool_calls, None);
6409 }
6410
6411 #[tokio::test]
6412 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6413 let registry = CapabilityRegistry::with_builtins();
6414
6415 let applied = apply_capabilities(
6417 RuntimeAgent::new("p", "gpt-5.2"),
6418 &["parallel_tool_calls".to_string()],
6419 ®istry,
6420 &test_ctx(),
6421 )
6422 .await;
6423 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6424
6425 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6427 base.parallel_tool_calls = Some(false);
6428 let applied = apply_capabilities(
6429 base,
6430 &["parallel_tool_calls".to_string()],
6431 ®istry,
6432 &test_ctx(),
6433 )
6434 .await;
6435 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6436 }
6437
6438 struct SkillContributingCapability;
6443
6444 impl Capability for SkillContributingCapability {
6445 fn id(&self) -> &str {
6446 "contributes_skills"
6447 }
6448 fn name(&self) -> &str {
6449 "Contributes Skills"
6450 }
6451 fn description(&self) -> &str {
6452 "Test capability that contributes skills."
6453 }
6454 fn contribute_skills(&self) -> Vec<SkillContribution> {
6455 vec![
6456 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6457 .with_files(vec![(
6458 "scripts/a.sh".to_string(),
6459 "#!/bin/sh\necho a\n".to_string(),
6460 )]),
6461 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6462 .with_user_invocable(false),
6463 ]
6464 }
6465 }
6466
6467 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6468 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6469 MountSource::InlineFile { content, .. } => content.as_str(),
6470 _ => panic!("Expected InlineFile for SKILL.md"),
6471 }
6472 }
6473
6474 #[tokio::test]
6475 async fn test_contribute_skills_normalized_to_mounts() {
6476 let mut registry = CapabilityRegistry::new();
6477 registry.register(SkillContributingCapability);
6478
6479 let configs = vec![AgentCapabilityConfig {
6480 capability_ref: CapabilityId::new("contributes_skills"),
6481 config: serde_json::json!({}),
6482 }];
6483
6484 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6485
6486 let skill_mounts: Vec<_> = collected
6487 .mounts
6488 .iter()
6489 .filter(|m| m.path.starts_with("/.agents/skills/"))
6490 .collect();
6491 assert_eq!(skill_mounts.len(), 2);
6492
6493 for m in &skill_mounts {
6496 assert!(m.is_readonly());
6497 assert_eq!(m.capability_id, "contributes_skills");
6498 }
6499
6500 let alpha = skill_mounts
6501 .iter()
6502 .find(|m| m.path == "/.agents/skills/alpha-skill")
6503 .expect("alpha-skill mount missing");
6504 match &alpha.source {
6505 MountSource::InlineDirectory { entries } => {
6506 assert!(entries.contains_key("SKILL.md"));
6507 assert!(entries.contains_key("scripts/a.sh"));
6508 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6509 assert_eq!(parsed.name, "alpha-skill");
6510 assert!(parsed.user_invocable);
6511 }
6512 _ => panic!("Expected InlineDirectory"),
6513 }
6514
6515 let beta = skill_mounts
6516 .iter()
6517 .find(|m| m.path == "/.agents/skills/beta-skill")
6518 .expect("beta-skill mount missing");
6519 match &beta.source {
6520 MountSource::InlineDirectory { entries } => {
6521 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6522 assert!(!parsed.user_invocable);
6523 }
6524 _ => panic!("Expected InlineDirectory"),
6525 }
6526 }
6527
6528 #[tokio::test]
6529 async fn test_contribute_skills_default_empty() {
6530 let mut registry = CapabilityRegistry::new();
6533 registry.register(FilterTestCapability { priority: 0 });
6534
6535 let configs = vec![AgentCapabilityConfig {
6536 capability_ref: CapabilityId::new("filter_test"),
6537 config: serde_json::json!({}),
6538 }];
6539
6540 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6541 assert!(
6542 collected
6543 .mounts
6544 .iter()
6545 .all(|m| !m.path.starts_with("/.agents/skills/"))
6546 );
6547 }
6548
6549 struct LocalizedCapability;
6550
6551 impl Capability for LocalizedCapability {
6552 fn id(&self) -> &str {
6553 "localized"
6554 }
6555 fn name(&self) -> &str {
6556 "Localized"
6557 }
6558 fn description(&self) -> &str {
6559 "English description"
6560 }
6561 fn localizations(&self) -> Vec<CapabilityLocalization> {
6562 vec![
6563 CapabilityLocalization {
6564 locale: "en",
6565 name: None,
6566 description: None,
6567 config_description: Some("Controls things."),
6568 config_overlay: None,
6569 },
6570 CapabilityLocalization {
6571 locale: "uk",
6572 name: Some("Локалізована"),
6573 description: Some("Український опис"),
6574 config_description: Some("Керує налаштуваннями."),
6575 config_overlay: None,
6576 },
6577 ]
6578 }
6579 }
6580
6581 #[test]
6582 fn localized_name_falls_back_exact_language_then_base() {
6583 let cap = LocalizedCapability;
6584 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6586 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6587 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6589 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6591 assert_eq!(cap.localized_name(None), "Localized");
6592 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6593 assert_eq!(cap.localized_description(Some("de")), "English description");
6594 }
6595
6596 #[test]
6597 fn describe_schema_resolves_config_description_per_locale() {
6598 let cap = LocalizedCapability;
6599 assert_eq!(
6600 cap.describe_schema(Some("uk-UA")).as_deref(),
6601 Some("Керує налаштуваннями.")
6602 );
6603 assert_eq!(
6605 cap.describe_schema(Some("pl")).as_deref(),
6606 Some("Controls things.")
6607 );
6608 assert_eq!(
6609 cap.describe_schema(None).as_deref(),
6610 Some("Controls things.")
6611 );
6612 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6614 }
6615}