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 claude_tool_search;
98pub mod compaction;
99mod current_time;
100mod data_knowledge;
101mod declarative;
102mod delegation_result;
103mod error_disclosure;
104pub mod facts;
105mod fake_aws;
106mod fake_crm;
107mod fake_financial;
108mod fake_warehouse;
109mod file_system;
110mod guardrails;
111mod human_intent;
112mod infinity_context;
113mod knowledge_base;
114mod knowledge_index;
115mod loop_detection;
116mod lua;
117mod lua_code_mode;
118pub mod mcp;
119mod memory;
120mod message_metadata;
121mod model_scout;
122mod monitors;
123mod noop;
124mod openai_tool_search;
125mod openrouter_server_tools;
126mod openrouter_workspace;
127#[cfg(feature = "ui-capabilities")]
128mod openui;
129mod parallel_tool_calls;
130mod platform_management;
131mod prompt_caching;
132mod prompt_canary_guardrail;
133mod research;
134mod sample_data;
135mod self_budget;
136mod session;
137mod session_sandbox;
138mod session_schedule;
139mod session_sql_database;
140mod session_storage;
141mod session_tasks;
142mod skills;
143mod skills_scoped;
144mod stateless_todo_list;
145mod subagents;
146mod system_commands;
147mod test_math;
148mod test_weather;
149mod tool_call_repair;
150mod tool_output_distillation;
151mod tool_output_persistence;
152mod tool_search;
153mod usage_limit_auto_continue;
154pub mod user_hooks;
155mod util;
156#[cfg(feature = "web-fetch")]
157mod web_fetch;
158
159pub const A2A_AGENT_DELEGATION_CAPABILITY_ID: &str = "a2a_agent_delegation";
164pub(crate) const AGENT_RUN_KEY_PREFIX: &str = "agent_run:";
168#[cfg(feature = "a2a")]
169pub use a2a_delegation::{A2aAgentDelegationCapability, SpawnAgentTool};
170#[cfg(feature = "ui-capabilities")]
171pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
172pub use agent_handoff::{
173 AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, SpawnAgentHandoffTool,
174};
175pub use agent_instructions::{
176 AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
177 AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
178 MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
179};
180pub use attach_skill::{
181 AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
182 SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
183 parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
184};
185pub use auto_tool_search::{AUTO_TOOL_SEARCH_CAPABILITY_ID, AutoToolSearchCapability};
186pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
187pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
188pub use budgeting::{BUDGETING_CAPABILITY_ID, BudgetingCapability};
189pub use claude_tool_search::{CLAUDE_TOOL_SEARCH_CAPABILITY_ID, ClaudeToolSearchCapability};
190pub use compaction::{
191 COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
192 CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
193 MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
194 SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
195 apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
196 build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
197 estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
198};
199pub use current_time::{CURRENT_TIME_CAPABILITY_ID, CurrentTimeCapability, GetCurrentTimeTool};
200pub use data_knowledge::{DATA_KNOWLEDGE_CAPABILITY_ID, DataKnowledgeCapability};
201pub use declarative::{
202 DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
203 DeclarativeCapabilitySkill, DeclarativeCapabilitySkillFile, declarative_capability_id,
204 declarative_capability_info, hydrate_declarative_capability_config,
205 hydrate_plugin_capability_config, is_declarative_capability, parse_declarative_capability_id,
206 plugin_capability_info, validate_declarative_capability_definition,
207};
208pub use delegation_result::{
209 ReportResultTool, ReportTaskProgressTool, report_result_tool_for_child_session,
210 report_task_progress_tool_for_child_session,
211};
212pub use error_disclosure::{
213 ERROR_DISCLOSURE_CAPABILITY_ID, ErrorDisclosureCapability, resolve_error_disclosure,
214};
215pub use facts::{FACTS_DYNAMIC_NOTE, Fact, FactsContext, Volatility, render_facts_block};
216pub use fake_aws::{
217 AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
218 AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
219 AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
220 AwsStopEc2InstanceTool, FAKE_AWS_CAPABILITY_ID, FakeAwsCapability,
221};
222pub use fake_crm::{
223 CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
224 CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
225 FAKE_CRM_CAPABILITY_ID, FakeCrmCapability,
226};
227pub use fake_financial::{
228 FAKE_FINANCIAL_CAPABILITY_ID, FakeFinancialCapability, FinanceCreateBudgetTool,
229 FinanceCreateTransactionTool, FinanceForecastCashFlowTool, FinanceGetBalanceTool,
230 FinanceGetExpenseReportTool, FinanceGetRevenueReportTool, FinanceListBudgetsTool,
231 FinanceListTransactionsTool,
232};
233pub use fake_warehouse::{
234 FAKE_WAREHOUSE_CAPABILITY_ID, FakeWarehouseCapability, WarehouseCreateInvoiceTool,
235 WarehouseCreateOrderTool, WarehouseCreateShipmentTool, WarehouseGetInventoryTool,
236 WarehouseInventoryReportTool, WarehouseListOrdersTool, WarehouseListShipmentsTool,
237 WarehouseProcessReturnTool, WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
238};
239pub use file_system::{
240 DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
241 ReadFileTool, SESSION_FILE_SYSTEM_CAPABILITY_ID, StatFileTool, WriteFileTool,
242};
243pub use guardrails::{GUARDRAILS_CAPABILITY_ID, GuardrailsCapability};
244pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
245pub use infinity_context::{
246 INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
247};
248pub use knowledge_base::{
249 KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
250 validate_knowledge_base_config,
251};
252pub use knowledge_index::{
253 KNOWLEDGE_INDEX_CAPABILITY_ID, KnowledgeIndexCapability, KnowledgeIndexConfig,
254 validate_knowledge_index_config,
255};
256pub use loop_detection::{LOOP_DETECTION_CAPABILITY_ID, LoopDetectionCapability};
257pub use lua::{LUA_CAPABILITY_ID, LuaCapability, LuaTool, LuaVfs, is_code_mode_eligible};
258pub use lua_code_mode::{LUA_CODE_MODE_CAPABILITY_ID, LuaCodeModeCapability};
259pub use mcp::{
260 MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
261 parse_mcp_capability_id,
262};
263pub use memory::{MEMORY_CAPABILITY_ID, MemoryCapability};
264pub use message_metadata::{
265 MESSAGE_METADATA_CAPABILITY_ID, MessageMetadataCapability, MessageMetadataConfig,
266 MessageMetadataField, render_annotation, strip_leading_timestamp_annotations,
267};
268pub use model_scout::{
269 MODEL_SCOUT_CAPABILITY_ID, ModelRanking, ModelScoutCapability, ProbeResult, ProbeTask,
270 RouterUpdateProposal, compute_score, rank_results,
271};
272pub use noop::{NOOP_CAPABILITY_ID, NoopCapability};
273pub use openai_tool_search::{
274 DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
275 model_supports_native_tool_search,
276};
277pub use openrouter_server_tools::{
278 OPENROUTER_SERVER_TOOLS_CAPABILITY_ID, OpenRouterServerToolsCapability,
279};
280pub use openrouter_workspace::{
281 OPENROUTER_WORKSPACE_CAPABILITY_ID, OpenRouterKeyInfo, OpenRouterRateLimit,
282 OpenRouterWorkspaceCapability, PolicyCompatibilityReport, WorkspacePolicyDrift,
283 detect_policy_drift,
284};
285#[cfg(feature = "ui-capabilities")]
286pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
287pub use parallel_tool_calls::{
288 PARALLEL_TOOL_CALLS_CAPABILITY_ID, ParallelToolCallsCapability, ParallelToolCallsMode,
289 parallel_tool_calls_from_config,
290};
291pub use platform_management::{
292 ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PLATFORM_MANAGEMENT_CAPABILITY_ID,
293 PlatformManagementCapability, ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool,
294 ReadSessionsTool, SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
295};
296pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
297pub use prompt_canary_guardrail::{
298 DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
299 PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
300 REASON_CODE_SYSTEM_PROMPT_LEAK,
301};
302pub use research::{RESEARCH_CAPABILITY_ID, ResearchCapability};
303pub use sample_data::{SAMPLE_DATA_CAPABILITY_ID, SampleDataCapability};
304pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
305pub use session::{
306 GetSessionInfoTool, SESSION_CAPABILITY_ID, SessionCapability, WriteSessionTitleTool,
307};
308pub use session_sandbox::{
309 SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
310 SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
311};
312pub use session_schedule::{
313 CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
314 SessionScheduleCapability,
315};
316pub use session_sql_database::{
317 SESSION_SQL_DATABASE_CAPABILITY_ID, SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool,
318 SqlSchemaTool,
319};
320pub use session_storage::{
321 KvStoreTool, SESSION_STORAGE_CAPABILITY_ID, SecretStoreTool, SessionStorageCapability,
322 is_internal_session_kv_key,
323};
324pub use session_tasks::{SESSION_TASKS_CAPABILITY_ID, SessionTasksCapability};
325pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
326pub use skills_scoped::{
327 ScopedSkillsCapability, SkillDirResolver, SkillScope, SkillsConfig, VfsSkillDirResolver,
328};
329pub use stateless_todo_list::{
330 STATELESS_TODO_LIST_CAPABILITY_ID, StatelessTodoListCapability, WriteTodosTool,
331};
332pub(crate) use subagents::SPAWN_AGENT_CONCURRENCY_CLASS;
333pub use subagents::{SUBAGENTS_CAPABILITY_ID, SpawnSubagentAsAgentTool, SubagentCapability};
334pub use usage_limit_auto_continue::{
335 AutoContinueConfig, USAGE_LIMIT_AUTO_CONTINUE_CAPABILITY_ID, UsageLimitAutoContinueCapability,
336 resolve_usage_limit_auto_continue,
337};
338pub use bashkit_shell::{
340 BASHKIT_SHELL_CAPABILITY_ID, BashTool, BashkitShellCapability, SessionFileSystemAdapter,
341};
342pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
343pub use test_math::{
344 AddTool, DivideTool, MultiplyTool, SubtractTool, TEST_MATH_CAPABILITY_ID, TestMathCapability,
345};
346pub use test_weather::{
347 GetForecastTool, GetWeatherTool, TEST_WEATHER_CAPABILITY_ID, TestWeatherCapability,
348};
349pub use tool_call_repair::{
350 DEFAULT_MAX_REPROMPTS, MAX_SALVAGE_INPUT_BYTES, RepairOutcome, SalvageResult,
351 TOOL_CALL_REPAIR_CAPABILITY_ID, ToolCallRepairCapability, ToolCallRepairConfig,
352 salvage_tool_arguments, tool_call_repair_capability,
353};
354pub use tool_output_distillation::{
355 DistillOutputHook, TOOL_OUTPUT_DISTILLATION_CAPABILITY_ID, ToolOutputDistillationCapability,
356};
357pub use tool_output_persistence::{
358 PersistOutputHook, TOOL_OUTPUT_PERSISTENCE_CAPABILITY_ID, ToolOutputPersistenceCapability,
359};
360pub use tool_search::{
361 TOOL_SEARCH_CAPABILITY_ID, TOOL_SEARCH_TOOL_NAME, ToolSearchCapability, ToolSearchTool,
362};
363pub use user_hooks::{USER_HOOKS_CAPABILITY_ID, UserHooksCapability};
364#[cfg(feature = "web-fetch")]
365pub use web_fetch::{
366 BotAuthPublicKey, WEB_FETCH_CAPABILITY_ID, WebFetchCapability, WebFetchTool,
367 derive_bot_auth_public_key,
368};
369
370pub struct SystemPromptContext {
380 pub session_id: SessionId,
382 pub locale: Option<String>,
384 pub file_store: Option<Arc<dyn SessionFileSystem>>,
386 pub model: Option<String>,
392}
393
394impl SystemPromptContext {
395 pub fn without_file_store(session_id: SessionId) -> Self {
397 Self {
398 session_id,
399 locale: None,
400 file_store: None,
401 model: None,
402 }
403 }
404
405 pub fn with_model(mut self, model: impl Into<String>) -> Self {
407 self.model = Some(model.into());
408 self
409 }
410}
411
412#[derive(Debug, Clone)]
464pub struct CapabilityLocalization {
465 pub locale: &'static str,
467 pub name: Option<&'static str>,
469 pub description: Option<&'static str>,
471 pub config_description: Option<&'static str>,
476 pub config_overlay: Option<serde_json::Value>,
482}
483
484impl CapabilityLocalization {
485 pub fn text(locale: &'static str, name: &'static str, description: &'static str) -> Self {
487 Self {
488 locale,
489 name: Some(name),
490 description: Some(description),
491 config_description: None,
492 config_overlay: None,
493 }
494 }
495}
496
497pub fn resolve_localized_field<T>(
501 localizations: &[CapabilityLocalization],
502 locale: Option<&str>,
503 field: impl Fn(&CapabilityLocalization) -> Option<T>,
504) -> Option<T> {
505 let mut candidates: Vec<String> = Vec::new();
506 if let Some(raw) = locale {
507 let normalized = raw.trim().replace('_', "-").to_lowercase();
508 if !normalized.is_empty() {
509 if let Some((language, _)) = normalized.split_once('-') {
510 let language = language.to_string();
511 candidates.push(normalized);
512 candidates.push(language);
513 } else {
514 candidates.push(normalized);
515 }
516 }
517 }
518 candidates.push("en".to_string());
519
520 for candidate in candidates {
521 let hit = localizations
522 .iter()
523 .find(|entry| entry.locale.eq_ignore_ascii_case(&candidate))
524 .and_then(&field);
525 if hit.is_some() {
526 return hit;
527 }
528 }
529 None
530}
531
532#[async_trait]
533pub trait Capability: Send + Sync {
534 fn id(&self) -> &str;
536
537 fn aliases(&self) -> Vec<&'static str> {
546 vec![]
547 }
548
549 fn name(&self) -> &str;
551
552 fn description(&self) -> &str;
554
555 fn localizations(&self) -> Vec<CapabilityLocalization> {
560 vec![]
561 }
562
563 fn localized_name(&self, locale: Option<&str>) -> String {
566 resolve_localized_field(&self.localizations(), locale, |entry| entry.name)
567 .unwrap_or_else(|| self.name())
568 .to_string()
569 }
570
571 fn localized_description(&self, locale: Option<&str>) -> String {
573 resolve_localized_field(&self.localizations(), locale, |entry| entry.description)
574 .unwrap_or_else(|| self.description())
575 .to_string()
576 }
577
578 fn describe_schema(&self, locale: Option<&str>) -> Option<String> {
582 resolve_localized_field(&self.localizations(), locale, |entry| {
583 entry.config_description
584 })
585 .map(str::to_string)
586 }
587
588 fn status(&self) -> CapabilityStatus {
590 CapabilityStatus::Available
591 }
592
593 fn icon(&self) -> Option<&str> {
595 None
596 }
597
598 fn category(&self) -> Option<&str> {
600 None
601 }
602
603 fn is_guardrail(&self) -> bool {
608 false
609 }
610
611 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
622 None
623 }
624
625 fn system_prompt_addition(&self) -> Option<&str> {
645 None
646 }
647
648 async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
660 self.system_prompt_addition().map(|addition| {
661 format!(
662 "<capability id=\"{}\">\n{}\n</capability>",
663 self.id(),
664 addition
665 )
666 })
667 }
668
669 fn system_prompt_preview(&self) -> Option<String> {
675 self.system_prompt_addition().map(|s| s.to_string())
676 }
677
678 fn tools(&self) -> Vec<Box<dyn Tool>> {
680 vec![]
681 }
682
683 fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
691 self.tools()
692 }
693
694 async fn system_prompt_contribution_with_config(
701 &self,
702 ctx: &SystemPromptContext,
703 _config: &serde_json::Value,
704 ) -> Option<String> {
705 self.system_prompt_contribution(ctx).await
706 }
707
708 fn tool_definitions(&self) -> Vec<ToolDefinition> {
711 self.tools().iter().map(|t| t.to_definition()).collect()
712 }
713
714 fn mounts(&self) -> Vec<MountPoint> {
722 vec![]
723 }
724
725 fn dependencies(&self) -> Vec<&'static str> {
734 vec![]
735 }
736
737 fn features(&self) -> Vec<&'static str> {
752 vec![]
753 }
754
755 fn config_schema(&self) -> Option<serde_json::Value> {
761 None
762 }
763
764 fn config_ui_schema(&self) -> Option<serde_json::Value> {
769 None
770 }
771
772 fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
778 Ok(())
779 }
780
781 fn mcp_servers(&self) -> ScopedMcpServers {
787 ScopedMcpServers::default()
788 }
789
790 fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
792 self.mcp_servers()
793 }
794
795 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
808 None
809 }
810
811 fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
819 None
820 }
821
822 fn llm_error_hook(&self) -> Option<Arc<dyn crate::llm_error_hook::LlmErrorHook>> {
834 None
835 }
836
837 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
852 vec![]
853 }
854
855 fn pre_tool_use_hooks(&self) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
866 vec![]
867 }
868
869 fn pre_tool_use_hooks_with_config(
874 &self,
875 _config: &serde_json::Value,
876 ) -> Vec<Arc<dyn crate::atoms::PreToolUseHook>> {
877 self.pre_tool_use_hooks()
878 }
879
880 fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
888 vec![]
889 }
890
891 fn post_tool_exec_hooks_with_config(
896 &self,
897 _config: &serde_json::Value,
898 ) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
899 self.post_tool_exec_hooks()
900 }
901
902 fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
911 vec![]
912 }
913
914 fn tool_definition_hooks_with_config(
919 &self,
920 _config: &serde_json::Value,
921 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
922 self.tool_definition_hooks()
923 }
924
925 fn tool_definition_hooks_with_context(
935 &self,
936 _ctx: &SystemPromptContext,
937 config: &serde_json::Value,
938 ) -> Vec<Arc<dyn ToolDefinitionHook>> {
939 self.tool_definition_hooks_with_config(config)
940 }
941
942 fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
950 vec![]
951 }
952
953 fn narrate(
967 &self,
968 _tool_def: Option<&ToolDefinition>,
969 tool_call: &ToolCall,
970 phase: crate::tool_narration::ToolNarrationPhase,
971 locale: Option<&str>,
972 ctx: crate::tool_narration::ToolNarrationContext<'_>,
973 ) -> Option<String> {
974 self.tools()
975 .iter()
976 .find(|tool| tool.name() == tool_call.name)
977 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
978 }
979
980 fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
996 vec![]
997 }
998
999 fn user_hooks_with_config(
1005 &self,
1006 _config: &serde_json::Value,
1007 ) -> Vec<crate::user_hook_types::UserHookSpec> {
1008 self.user_hooks()
1009 }
1010
1011 fn risk_level(&self) -> RiskLevel {
1019 RiskLevel::Low
1020 }
1021
1022 fn commands(&self) -> Vec<CommandDescriptor> {
1030 vec![]
1031 }
1032
1033 async fn execute_command(
1047 &self,
1048 request: &ExecuteCommandRequest,
1049 _ctx: &CommandExecutionContext,
1050 ) -> crate::error::Result<CommandResult> {
1051 Err(crate::error::AgentLoopError::config(format!(
1052 "capability {} declared command /{} but does not implement execute_command",
1053 self.id(),
1054 request.name,
1055 )))
1056 }
1057
1058 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
1067 vec![]
1068 }
1069
1070 fn contribute_skills(&self) -> Vec<SkillContribution> {
1080 vec![]
1081 }
1082
1083 fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
1094 vec![]
1095 }
1096
1097 fn post_output_guardrails_with_config(
1109 &self,
1110 _config: &serde_json::Value,
1111 ) -> Vec<Arc<dyn crate::output_guardrail::PostGenerationOutputGuardrail>> {
1112 vec![]
1113 }
1114}
1115
1116pub trait ToolDefinitionHook: Send + Sync {
1117 fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
1118
1119 fn applies_with_native_tool_search(&self) -> bool {
1124 true
1125 }
1126}
1127
1128pub trait ToolCallHook: Send + Sync {
1129 fn narration(
1130 &self,
1131 _tool_def: Option<&ToolDefinition>,
1132 _tool_call: &ToolCall,
1133 _phase: crate::tool_narration::ToolNarrationPhase,
1134 _locale: Option<&str>,
1135 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
1136 ) -> Option<String> {
1137 None
1138 }
1139
1140 fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
1141 tool_call
1142 }
1143}
1144
1145pub struct CapabilityNarrationHook(pub Arc<dyn Capability>);
1151
1152impl ToolCallHook for CapabilityNarrationHook {
1153 fn narration(
1154 &self,
1155 tool_def: Option<&ToolDefinition>,
1156 tool_call: &ToolCall,
1157 phase: crate::tool_narration::ToolNarrationPhase,
1158 locale: Option<&str>,
1159 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1160 ) -> Option<String> {
1161 self.0.narrate(tool_def, tool_call, phase, locale, ctx)
1162 }
1163}
1164
1165#[derive(
1169 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
1170)]
1171#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1172#[cfg_attr(feature = "openapi", schema(example = "low"))]
1173#[serde(rename_all = "lowercase")]
1174pub enum RiskLevel {
1175 Low,
1177 Medium,
1179 High,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1189#[serde(rename_all = "snake_case")]
1190pub enum BlueprintModel {
1191 Fixed(String),
1193 Default(String),
1195 Inherit,
1197}
1198
1199pub struct AgentBlueprint {
1205 pub id: &'static str,
1207 pub name: &'static str,
1209 pub description: &'static str,
1211 pub model: BlueprintModel,
1213 pub system_prompt: &'static str,
1215 pub tools: Vec<Box<dyn Tool>>,
1217 pub max_turns: Option<usize>,
1219 pub config_schema: Option<serde_json::Value>,
1221}
1222
1223impl AgentBlueprint {
1224 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
1226 self.tools.iter().map(|t| t.to_definition()).collect()
1227 }
1228}
1229
1230impl std::fmt::Debug for AgentBlueprint {
1231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1232 f.debug_struct("AgentBlueprint")
1233 .field("id", &self.id)
1234 .field("name", &self.name)
1235 .field("model", &self.model)
1236 .field("tool_count", &self.tools.len())
1237 .field("max_turns", &self.max_turns)
1238 .finish()
1239 }
1240}
1241
1242#[derive(Clone)]
1269pub struct CapabilityRegistry {
1270 capabilities: HashMap<String, Arc<dyn Capability>>,
1271 aliases: HashMap<String, String>,
1273}
1274
1275impl CapabilityRegistry {
1276 pub fn new() -> Self {
1278 Self {
1279 capabilities: HashMap::new(),
1280 aliases: HashMap::new(),
1281 }
1282 }
1283
1284 pub fn with_builtins() -> Self {
1289 Self::with_builtins_for_grade(DeploymentGrade::from_env())
1290 }
1291
1292 pub fn runtime_builtins() -> Self {
1303 let mut registry = Self::new();
1304
1305 registry.register(AgentInstructionsCapability);
1306 registry.register(HumanIntentCapability);
1307 registry.register(NoopCapability);
1308 registry.register(CurrentTimeCapability);
1309 registry.register(MessageMetadataCapability);
1310 registry.register(FileSystemCapability);
1311 registry.register(SessionStorageCapability);
1312 registry.register(SessionCapability);
1313 registry.register(StatelessTodoListCapability);
1314 #[cfg(feature = "web-fetch")]
1315 registry.register(WebFetchCapability::from_env());
1316 registry.register(BashkitShellCapability);
1317 registry.register(BtwCapability);
1318 registry.register(InfinityContextCapability);
1319 registry.register(budgeting::BudgetingCapability);
1320 registry.register(SelfBudgetCapability);
1321 registry.register(CompactionCapability);
1322 registry.register(ErrorDisclosureCapability);
1323 registry.register(OpenAiToolSearchCapability::new());
1324 registry.register(ClaudeToolSearchCapability::new());
1325 registry.register(ToolSearchCapability::new());
1326 registry.register(AutoToolSearchCapability::new());
1327 registry.register(PromptCachingCapability::new());
1328 registry.register(ParallelToolCallsCapability);
1329 registry.register(SkillsCapability);
1330 registry.register(SystemCommandsCapability);
1331 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1332 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1333 registry.register(LoopDetectionCapability);
1334 registry.register(ToolCallRepairCapability);
1335 registry.register(PromptCanaryGuardrailCapability);
1336 registry.register(GuardrailsCapability);
1337 registry.register(user_hooks::UserHooksCapability);
1338
1339 let internal_flags = crate::InternalFeatureFlags::from_env();
1340 if internal_flags.lua {
1341 registry.register(LuaCapability);
1342 registry.register(LuaCodeModeCapability);
1343 }
1344
1345 registry
1346 }
1347
1348 pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
1353 let mut registry = Self::new();
1354
1355 registry.register(AgentInstructionsCapability);
1357 registry.register(HumanIntentCapability);
1358 registry.register(NoopCapability);
1359 registry.register(CurrentTimeCapability);
1360 registry.register(MessageMetadataCapability);
1361 registry.register(ResearchCapability);
1362 registry.register(ModelScoutCapability);
1363 registry.register(OpenRouterWorkspaceCapability);
1364 registry.register(OpenRouterServerToolsCapability);
1365 registry.register(PlatformManagementCapability);
1366 registry.register(FileSystemCapability);
1367 registry.register(MemoryCapability);
1368 registry.register(SessionStorageCapability);
1369 registry.register(SessionCapability);
1370 registry.register(SessionSqlDatabaseCapability);
1371 registry.register(TestMathCapability);
1372 registry.register(TestWeatherCapability);
1373 registry.register(StatelessTodoListCapability);
1374 #[cfg(feature = "web-fetch")]
1375 registry.register(WebFetchCapability::from_env());
1376 registry.register(BashkitShellCapability);
1377 registry.register(BackgroundExecutionCapability);
1378 registry.register(SessionScheduleCapability);
1379 registry.register(BtwCapability);
1380 registry.register(InfinityContextCapability);
1381 registry.register(budgeting::BudgetingCapability);
1382 registry.register(SelfBudgetCapability);
1383 registry.register(CompactionCapability);
1384 registry.register(ErrorDisclosureCapability);
1385
1386 registry.register(OpenAiToolSearchCapability::new());
1388 registry.register(ClaudeToolSearchCapability::new());
1390 registry.register(ToolSearchCapability::new());
1392 registry.register(AutoToolSearchCapability::new());
1394 registry.register(PromptCachingCapability::new());
1395
1396 registry.register(ParallelToolCallsCapability);
1398
1399 registry.register(SkillsCapability);
1401
1402 registry.register(SubagentCapability);
1404
1405 registry.register(SessionTasksCapability);
1407
1408 if crate::FeatureFlags::from_env(&grade).agent_delegation {
1412 registry.register(AgentHandoffCapability);
1413 #[cfg(feature = "a2a")]
1417 registry.register(A2aAgentDelegationCapability);
1418 }
1419
1420 registry.register(SystemCommandsCapability);
1422
1423 registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
1425 registry.register(tool_output_distillation::ToolOutputDistillationCapability);
1426
1427 registry.register(user_hooks::UserHooksCapability);
1430
1431 registry.register(LoopDetectionCapability);
1433
1434 registry.register(UsageLimitAutoContinueCapability);
1441
1442 registry.register(ToolCallRepairCapability);
1446
1447 registry.register(PromptCanaryGuardrailCapability);
1450
1451 registry.register(GuardrailsCapability);
1454
1455 #[cfg(feature = "ui-capabilities")]
1457 {
1458 registry.register(OpenUiCapability);
1459 registry.register(A2UiCapability);
1460 }
1461
1462 registry.register(SampleDataCapability);
1464
1465 registry.register(DataKnowledgeCapability);
1467
1468 registry.register(KnowledgeBaseCapability);
1470
1471 registry.register(KnowledgeIndexCapability);
1473
1474 registry.register(FakeWarehouseCapability);
1476 registry.register(FakeAwsCapability);
1477 registry.register(FakeCrmCapability);
1478 registry.register(FakeFinancialCapability);
1479
1480 let internal_flags = crate::InternalFeatureFlags::from_env();
1482 if internal_flags.session_sandbox {
1483 registry.register(SessionSandboxCapability);
1484 }
1485
1486 if internal_flags.lua {
1490 registry.register(LuaCapability);
1491 registry.register(LuaCodeModeCapability);
1494 }
1495 for plugin in inventory::iter::<IntegrationPlugin>() {
1496 if (!plugin.experimental_only || grade.experimental_features_enabled())
1497 && plugin
1498 .feature_flag
1499 .is_none_or(|f| internal_flags.is_enabled(f))
1500 {
1501 registry.register_boxed((plugin.factory)());
1502 }
1503 }
1504
1505 registry
1506 }
1507
1508 pub fn register(&mut self, capability: impl Capability + 'static) {
1510 self.register_arc(Arc::new(capability));
1511 }
1512
1513 pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
1515 self.register_arc(Arc::from(capability));
1516 }
1517
1518 pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
1520 let canonical = capability.id().to_string();
1521 for alias in capability.aliases() {
1522 self.aliases.insert(alias.to_string(), canonical.clone());
1523 }
1524 self.capabilities.insert(canonical, capability);
1525 }
1526
1527 pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
1529 self.capabilities
1530 .get(id)
1531 .or_else(|| self.aliases.get(id).and_then(|c| self.capabilities.get(c)))
1532 }
1533
1534 pub fn canonical_id<'a>(&'a self, id: &'a str) -> Option<&'a str> {
1539 if self.capabilities.contains_key(id) {
1540 Some(id)
1541 } else {
1542 self.aliases
1543 .get(id)
1544 .filter(|c| self.capabilities.contains_key(*c))
1545 .map(String::as_str)
1546 }
1547 }
1548
1549 pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
1551 let canonical = self.canonical_id(id)?.to_string();
1552 let removed = self.capabilities.remove(&canonical);
1553 self.aliases.retain(|_, target| *target != canonical);
1554 removed
1555 }
1556
1557 pub fn has(&self, id: &str) -> bool {
1559 self.get(id).is_some()
1560 }
1561
1562 pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1564 self.capabilities.values().collect()
1565 }
1566
1567 pub fn len(&self) -> usize {
1569 self.capabilities.len()
1570 }
1571
1572 pub fn is_empty(&self) -> bool {
1574 self.capabilities.is_empty()
1575 }
1576
1577 pub fn builder() -> CapabilityRegistryBuilder {
1579 CapabilityRegistryBuilder::new()
1580 }
1581
1582 pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1586 for cap in self.capabilities.values() {
1587 for bp in cap.agent_blueprints() {
1588 if bp.id == id {
1589 return Some(bp);
1590 }
1591 }
1592 }
1593 None
1594 }
1595
1596 pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1600 for (capability_id, cap) in &self.capabilities {
1601 for bp in cap.agent_blueprints() {
1602 if bp.id == id {
1603 return Some((capability_id.clone(), bp));
1604 }
1605 }
1606 }
1607 None
1608 }
1609
1610 pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1612 self.capabilities
1613 .values()
1614 .flat_map(|cap| cap.agent_blueprints())
1615 .collect()
1616 }
1617}
1618
1619impl Default for CapabilityRegistry {
1620 fn default() -> Self {
1621 Self::with_builtins()
1622 }
1623}
1624
1625impl std::fmt::Debug for CapabilityRegistry {
1626 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1627 let ids: Vec<_> = self.capabilities.keys().collect();
1628 f.debug_struct("CapabilityRegistry")
1629 .field("capabilities", &ids)
1630 .finish()
1631 }
1632}
1633
1634pub struct CapabilityRegistryBuilder {
1636 registry: CapabilityRegistry,
1637}
1638
1639impl CapabilityRegistryBuilder {
1640 pub fn new() -> Self {
1642 Self {
1643 registry: CapabilityRegistry::new(),
1644 }
1645 }
1646
1647 pub fn with_builtins() -> Self {
1649 Self {
1650 registry: CapabilityRegistry::with_builtins(),
1651 }
1652 }
1653
1654 pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1656 self.registry.register(capability);
1657 self
1658 }
1659
1660 pub fn build(self) -> CapabilityRegistry {
1662 self.registry
1663 }
1664}
1665
1666impl Default for CapabilityRegistryBuilder {
1667 fn default() -> Self {
1668 Self::new()
1669 }
1670}
1671
1672pub struct ModelViewContext<'a> {
1678 pub session_id: SessionId,
1679 pub prior_usage: Option<&'a TokenUsage>,
1680}
1681
1682pub trait ModelViewProvider: Send + Sync {
1688 fn apply_model_view(
1689 &self,
1690 messages: Vec<Message>,
1691 config: &serde_json::Value,
1692 context: &ModelViewContext<'_>,
1693 ) -> Vec<Message>;
1694
1695 fn priority(&self) -> i32 {
1696 0
1697 }
1698}
1699
1700pub struct CollectedCapabilities {
1705 pub system_prompt_parts: Vec<String>,
1707 pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1709 pub tools: Vec<Box<dyn Tool>>,
1711 pub tool_definitions: Vec<ToolDefinition>,
1713 pub mounts: Vec<MountPoint>,
1715 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1717 pub applied_ids: Vec<String>,
1719 pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
1721 pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
1723 pub openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig>,
1726 pub parallel_tool_calls: Option<bool>,
1730 pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1732 pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1734 pub mcp_servers: ScopedMcpServers,
1736 }
1742
1743#[derive(Debug, Clone, PartialEq, Eq)]
1744pub struct SystemPromptAttribution {
1745 pub capability_id: String,
1746 pub content: String,
1747}
1748
1749impl CollectedCapabilities {
1750 pub fn system_prompt_prefix(&self) -> Option<String> {
1753 if self.system_prompt_parts.is_empty() {
1754 None
1755 } else {
1756 Some(self.system_prompt_parts.join("\n\n"))
1757 }
1758 }
1759
1760 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1764 for (provider, config) in &self.message_filter_providers {
1766 provider.apply_filters(query, config);
1767 }
1768 }
1769
1770 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1773 for (provider, config) in &self.message_filter_providers {
1774 provider.post_load(messages, config);
1775 }
1776 }
1777
1778 pub fn has_message_filters(&self) -> bool {
1780 !self.message_filter_providers.is_empty()
1781 }
1782}
1783
1784struct SpawnAgentTargetProvider {
1785 target_type: &'static str,
1786 tool: Box<dyn Tool>,
1787}
1788
1789#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1791#[serde(rename_all = "snake_case")]
1792pub(crate) enum SpawnMode {
1793 Background,
1794 Foreground,
1795}
1796
1797impl SpawnMode {
1798 pub(crate) fn parse(value: &str) -> Option<Self> {
1799 match value {
1800 "background" => Some(Self::Background),
1801 "foreground" => Some(Self::Foreground),
1802 _ => None,
1803 }
1804 }
1805
1806 pub(crate) fn as_str(self) -> &'static str {
1807 match self {
1808 Self::Background => "background",
1809 Self::Foreground => "foreground",
1810 }
1811 }
1812}
1813
1814struct UnifiedSpawnAgentTool {
1815 providers: Vec<SpawnAgentTargetProvider>,
1816}
1817
1818impl UnifiedSpawnAgentTool {
1819 fn new(providers: Vec<SpawnAgentTargetProvider>) -> Self {
1820 Self { providers }
1821 }
1822
1823 fn provider_for(&self, target_type: &str) -> Option<&dyn Tool> {
1824 self.providers
1825 .iter()
1826 .find(|provider| provider.target_type == target_type)
1827 .map(|provider| provider.tool.as_ref())
1828 }
1829
1830 fn target_types(&self) -> Vec<&'static str> {
1831 ["subagent", "agent", "external_a2a"]
1832 .into_iter()
1833 .filter(|target_type| {
1834 self.providers
1835 .iter()
1836 .any(|provider| provider.target_type == *target_type)
1837 })
1838 .collect()
1839 }
1840
1841 fn target_constraint_branches(&self) -> Vec<serde_json::Value> {
1846 self.target_types()
1847 .into_iter()
1848 .filter_map(|target_type| match target_type {
1849 "subagent" => Some(serde_json::json!({
1850 "properties": {
1851 "type": {"const": "subagent"}
1852 }
1853 })),
1854 "agent" => Some(serde_json::json!({
1855 "properties": {
1856 "type": {"const": "agent"}
1857 },
1858 "required": ["type", "id"]
1859 })),
1860 "external_a2a" => Some(serde_json::json!({
1861 "properties": {
1862 "type": {"const": "external_a2a"}
1863 },
1864 "anyOf": [
1865 {"required": ["id"]},
1866 {"required": ["external_agent_id"]}
1867 ]
1868 })),
1869 _ => None,
1870 })
1871 .collect()
1872 }
1873
1874 }
1884
1885#[async_trait]
1886impl Tool for UnifiedSpawnAgentTool {
1887 fn narrate(
1888 &self,
1889 tool_call: &ToolCall,
1890 phase: crate::tool_narration::ToolNarrationPhase,
1891 locale: Option<&str>,
1892 ctx: crate::tool_narration::ToolNarrationContext<'_>,
1893 ) -> Option<String> {
1894 let target_type = tool_call
1895 .arguments
1896 .get("target")
1897 .and_then(|target| target.get("type"))
1898 .and_then(serde_json::Value::as_str)?;
1899 self.provider_for(target_type)
1900 .and_then(|tool| tool.narrate(tool_call, phase, locale, ctx))
1901 }
1902
1903 fn name(&self) -> &str {
1904 "spawn_agent"
1905 }
1906
1907 fn display_name(&self) -> Option<&str> {
1908 Some("Spawn Agent")
1909 }
1910
1911 fn description(&self) -> &str {
1912 "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."
1913 }
1914
1915 fn parameters_schema(&self) -> serde_json::Value {
1916 serde_json::json!({
1917 "type": "object",
1918 "properties": {
1919 "name": {
1920 "type": "string",
1921 "description": "Human-readable name for the delegated run (subagent, first-party handoff, or external delegation). Used as the task label."
1922 },
1923 "instructions": {
1924 "type": "string",
1925 "description": "Instructions for the delegated agent. Do not include credentials or bearer tokens."
1926 },
1927 "goal": {
1928 "type": "string",
1929 "description": "Optional objective stored on the spawned session and made visible at system-prompt level."
1930 },
1931 "lifetime": {
1932 "type": "string",
1933 "enum": ["linked", "detached"],
1934 "default": "linked",
1935 "description": "linked creates a lifecycle child; detached creates an independent top-level peer session. Not valid for external_a2a."
1936 },
1937 "seed": {
1938 "type": "string",
1939 "enum": ["fresh", "fork", "workspace"],
1940 "default": "fresh",
1941 "description": "Detached-session seed mode: fresh starts blank, fork copies history/workspace/session storage, workspace copies workspace files only."
1942 },
1943 "target": {
1944 "type": "object",
1945 "properties": {
1946 "type": {
1947 "type": "string",
1948 "enum": self.target_types(),
1949 "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."
1950 },
1951 "id": {
1952 "type": "string",
1953 "description": "Configured target id for first-party handoffs or external A2A agents."
1954 },
1955 "external_agent_id": {
1956 "type": "string",
1957 "description": "Configured external A2A agent id."
1958 }
1959 },
1960 "required": ["type"],
1961 "oneOf": self.target_constraint_branches(),
1962 "additionalProperties": false
1963 },
1964 "mode": {
1965 "type": "string",
1966 "enum": ["background", "foreground"],
1967 "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."
1968 },
1969 "blueprint": {
1970 "type": "string",
1971 "description": "Subagent-only blueprint ID to spawn a specialist agent with its own tools and model."
1972 },
1973 "config": {
1974 "type": "object",
1975 "description": "Subagent-only blueprint configuration. Only valid when blueprint is set."
1976 },
1977 "result_schema": {
1978 "type": "object",
1979 "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."
1980 },
1981 "message_schema": {
1982 "type": "object",
1983 "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."
1984 },
1985 "public_context": {
1986 "type": "object",
1987 "description": "Agent-handoff-only non-secret structured context to include with the instructions."
1988 },
1989 "wait_timeout_secs": {
1990 "type": "integer",
1991 "minimum": 1,
1992 "maximum": 86400,
1993 "description": "External-A2A-only foreground timeout."
1994 },
1995 "wake_on_completion": {
1996 "type": "boolean",
1997 "description": "External-A2A-only control for background completion wake-ups."
1998 }
1999 },
2000 "required": ["name", "instructions", "target"],
2001 "additionalProperties": false
2002 })
2003 }
2004
2005 fn hints(&self) -> crate::tool_types::ToolHints {
2006 let mut hints = crate::tool_types::ToolHints::default()
2007 .with_long_running(true)
2008 .with_concurrency_class(SPAWN_AGENT_CONCURRENCY_CLASS);
2009 if self.provider_for("external_a2a").is_some() {
2010 hints = hints.with_open_world(true);
2011 }
2012 hints
2013 }
2014
2015 async fn execute(&self, _arguments: serde_json::Value) -> ToolExecutionResult {
2016 ToolExecutionResult::tool_error(
2017 "spawn_agent requires context. This tool must be executed with session context.",
2018 )
2019 }
2020
2021 async fn execute_with_context(
2022 &self,
2023 arguments: serde_json::Value,
2024 context: &ToolContext,
2025 ) -> ToolExecutionResult {
2026 let target_type = match arguments
2027 .get("target")
2028 .and_then(|target| target.get("type"))
2029 .and_then(serde_json::Value::as_str)
2030 {
2031 Some(target_type) => target_type,
2032 None => {
2033 return ToolExecutionResult::tool_error("Missing required parameter: target.type");
2034 }
2035 };
2036
2037 let Some(provider) = self.provider_for(target_type) else {
2038 let supported = self.target_types().join(", ");
2039 return ToolExecutionResult::tool_error(format!(
2040 "Unsupported spawn_agent target.type: \"{target_type}\". Supported target types: {supported}"
2041 ));
2042 };
2043 if target_type == "external_a2a"
2044 && arguments
2045 .get("lifetime")
2046 .and_then(serde_json::Value::as_str)
2047 .is_some_and(|value| value == "detached")
2048 {
2049 return ToolExecutionResult::tool_error(
2050 "lifetime=\"detached\" is only valid for local session targets (subagent or agent), not external_a2a.",
2051 );
2052 }
2053 if target_type == "external_a2a"
2054 && arguments
2055 .get("message_schema")
2056 .is_some_and(|schema| !schema.is_null())
2057 {
2058 return ToolExecutionResult::tool_error(
2059 "message_schema is not supported for external_a2a targets because remote agents cannot receive report_task_progress.",
2060 );
2061 }
2062
2063 provider.execute_with_context(arguments, context).await
2064 }
2065
2066 fn requires_context(&self) -> bool {
2067 true
2068 }
2069}
2070
2071pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
2076 let Some(additions) = additions.filter(|value| !value.is_empty()) else {
2077 return base_system_prompt.to_string();
2078 };
2079
2080 if base_system_prompt.is_empty() {
2081 return additions.to_string();
2082 }
2083
2084 if base_system_prompt.contains("<system-prompt>") {
2085 format!("{base_system_prompt}\n\n{additions}")
2086 } else {
2087 format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
2088 }
2089}
2090
2091pub struct CollectedMessageFilters {
2098 pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
2100}
2101
2102pub struct CollectedModelViewProviders {
2104 pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
2106}
2107
2108impl CollectedMessageFilters {
2114 pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
2116 for (provider, config) in &self.message_filter_providers {
2117 provider.apply_filters(query, config);
2118 }
2119 }
2120
2121 pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
2123 for (provider, config) in &self.message_filter_providers {
2124 provider.post_load(messages, config);
2125 }
2126 }
2127}
2128
2129impl CollectedModelViewProviders {
2130 pub fn apply_model_view(
2132 &self,
2133 mut messages: Vec<Message>,
2134 context: &ModelViewContext<'_>,
2135 ) -> Vec<Message> {
2136 for (provider, config) in &self.model_view_providers {
2137 messages = provider.apply_model_view(messages, config, context);
2138 }
2139 messages
2140 }
2141}
2142
2143fn compaction_is_enabled(
2149 capability_configs: &[AgentCapabilityConfig],
2150 registry: &CapabilityRegistry,
2151) -> bool {
2152 capability_configs.iter().any(|cap_config| {
2153 cap_config.capability_ref.as_str() == COMPACTION_CAPABILITY_ID
2154 && registry
2155 .get(cap_config.capability_ref.as_str())
2156 .is_some_and(|cap| cap.status() == CapabilityStatus::Available)
2157 })
2158}
2159
2160fn message_filter_config_for(
2169 cap_id: &str,
2170 base: &serde_json::Value,
2171 compaction_on: bool,
2172) -> serde_json::Value {
2173 if cap_id != INFINITY_CONTEXT_CAPABILITY_ID || !compaction_on {
2174 return base.clone();
2175 }
2176 let mut config = base.clone();
2177 match config.as_object_mut() {
2178 Some(map) => {
2179 map.insert(
2180 "compaction_active".to_string(),
2181 serde_json::Value::Bool(true),
2182 );
2183 }
2184 None => {
2185 config = serde_json::json!({ "compaction_active": true });
2186 }
2187 }
2188 config
2189}
2190
2191pub fn collect_message_filters_only(
2197 capability_configs: &[AgentCapabilityConfig],
2198 registry: &CapabilityRegistry,
2199) -> CollectedMessageFilters {
2200 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2201 Vec::new();
2202 let compaction_on = compaction_is_enabled(capability_configs, registry);
2203
2204 for cap_config in capability_configs {
2205 let cap_id = cap_config.capability_ref.as_str();
2206 if let Some(capability) = registry.get(cap_id) {
2207 if capability.status() != CapabilityStatus::Available {
2208 continue;
2209 }
2210 let effective: &dyn Capability = capability
2213 .resolve_for_model(None)
2214 .unwrap_or_else(|| capability.as_ref());
2215 if let Some(provider) = effective.message_filter_provider() {
2216 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2217 message_filter_providers.push((provider, config));
2218 }
2219 }
2220 }
2221
2222 message_filter_providers.sort_by_key(|(p, _)| p.priority());
2223
2224 CollectedMessageFilters {
2225 message_filter_providers,
2226 }
2227}
2228
2229pub fn collect_model_view_providers(
2236 capability_configs: &[AgentCapabilityConfig],
2237 registry: &CapabilityRegistry,
2238 model: Option<&str>,
2239) -> CollectedModelViewProviders {
2240 let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
2241
2242 for cap_config in capability_configs {
2243 let cap_id = cap_config.capability_ref.as_str();
2244 if let Some(capability) = registry.get(cap_id) {
2245 if capability.status() != CapabilityStatus::Available {
2246 continue;
2247 }
2248 let effective: &dyn Capability = capability
2249 .resolve_for_model(model)
2250 .unwrap_or_else(|| capability.as_ref());
2251 if let Some(provider) = effective.model_view_provider() {
2252 model_view_providers.push((provider, cap_config.config.clone()));
2253 }
2254 }
2255 }
2256
2257 model_view_providers.sort_by_key(|(p, _)| p.priority());
2258
2259 CollectedModelViewProviders {
2260 model_view_providers,
2261 }
2262}
2263
2264pub fn collect_dynamic_facts(
2270 capability_configs: &[AgentCapabilityConfig],
2271 registry: &CapabilityRegistry,
2272 model: Option<&str>,
2273 ctx: &FactsContext,
2274) -> Vec<Fact> {
2275 let mut dynamic = Vec::new();
2276 for cap_config in capability_configs {
2277 let cap_id = cap_config.capability_ref.as_str();
2278 if let Some(capability) = registry.get(cap_id) {
2279 if capability.status() != CapabilityStatus::Available {
2280 continue;
2281 }
2282 let effective: &dyn Capability = capability
2283 .resolve_for_model(model)
2284 .unwrap_or_else(|| capability.as_ref());
2285 for fact in effective.facts(&cap_config.config, ctx) {
2286 if fact.volatility == Volatility::Dynamic {
2287 dynamic.push(fact);
2288 }
2289 }
2290 }
2291 }
2292 dynamic
2293}
2294
2295pub fn collect_capability_mcp_servers(
2296 capability_configs: &[AgentCapabilityConfig],
2297 registry: &CapabilityRegistry,
2298) -> ScopedMcpServers {
2299 let mut servers = ScopedMcpServers::default();
2300
2301 for cap_config in capability_configs {
2302 let cap_id = cap_config.capability_ref.as_str();
2303 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2306 if let Ok(definition) =
2307 serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
2308 {
2309 if definition.status != CapabilityStatus::Available {
2310 continue;
2311 }
2312 if let Some(contributed) = definition.mcp_servers {
2313 servers = merge_scoped_mcp_servers(&servers, &contributed);
2314 }
2315 }
2316 continue;
2317 }
2318 if let Some(capability) = registry.get(cap_id) {
2319 if capability.status() != CapabilityStatus::Available {
2320 continue;
2321 }
2322 servers = merge_scoped_mcp_servers(
2323 &servers,
2324 &capability.mcp_servers_with_config(&cap_config.config),
2325 );
2326 }
2327 }
2328
2329 servers
2330}
2331
2332pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
2339
2340#[derive(Debug, Clone, PartialEq, Eq)]
2342pub enum DependencyError {
2343 CircularDependency {
2345 capability_id: String,
2347 chain: Vec<String>,
2349 },
2350 TooManyCapabilities {
2352 count: usize,
2354 max: usize,
2356 },
2357}
2358
2359impl std::fmt::Display for DependencyError {
2360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2361 match self {
2362 DependencyError::CircularDependency {
2363 capability_id,
2364 chain,
2365 } => {
2366 write!(
2367 f,
2368 "Circular dependency detected: {} depends on itself via chain: {} -> {}",
2369 capability_id,
2370 chain.join(" -> "),
2371 capability_id
2372 )
2373 }
2374 DependencyError::TooManyCapabilities { count, max } => {
2375 write!(
2376 f,
2377 "Too many capabilities after resolution: {} (max: {})",
2378 count, max
2379 )
2380 }
2381 }
2382 }
2383}
2384
2385impl std::error::Error for DependencyError {}
2386
2387#[derive(Debug, Clone)]
2389pub struct ResolvedCapabilities {
2390 pub resolved_ids: Vec<String>,
2393 pub added_as_dependencies: Vec<String>,
2395 pub user_selected: Vec<String>,
2397}
2398
2399pub fn resolve_dependencies(
2419 selected_ids: &[String],
2420 registry: &CapabilityRegistry,
2421) -> Result<ResolvedCapabilities, DependencyError> {
2422 use std::collections::HashSet;
2423
2424 let user_selected: HashSet<String> = selected_ids
2426 .iter()
2427 .map(|id| registry.canonical_id(id).unwrap_or(id).to_string())
2428 .collect();
2429 let mut resolved: Vec<String> = Vec::new();
2430 let mut resolved_set: HashSet<String> = HashSet::new();
2431 let mut added_as_dependencies: Vec<String> = Vec::new();
2432
2433 for cap_id in selected_ids {
2435 resolve_single_capability(
2436 cap_id,
2437 registry,
2438 &mut resolved,
2439 &mut resolved_set,
2440 &mut added_as_dependencies,
2441 &user_selected,
2442 &mut Vec::new(), )?;
2444 }
2445
2446 if resolved.len() > MAX_RESOLVED_CAPABILITIES {
2448 return Err(DependencyError::TooManyCapabilities {
2449 count: resolved.len(),
2450 max: MAX_RESOLVED_CAPABILITIES,
2451 });
2452 }
2453
2454 Ok(ResolvedCapabilities {
2455 resolved_ids: resolved,
2456 added_as_dependencies,
2457 user_selected: selected_ids.to_vec(),
2458 })
2459}
2460
2461pub fn resolve_capability_configs(
2466 selected_configs: &[AgentCapabilityConfig],
2467 registry: &CapabilityRegistry,
2468) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
2469 let mut selected_ids: Vec<String> = Vec::new();
2470 for config in selected_configs {
2471 if (is_declarative_capability(config.capability_id())
2474 || is_plugin_capability(config.capability_id()))
2475 && let Ok(definition) =
2476 serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
2477 {
2478 selected_ids.extend(definition.dependencies);
2479 }
2480 selected_ids.push(config.capability_id().to_string());
2481 }
2482 let resolved = resolve_dependencies(&selected_ids, registry)?;
2483
2484 let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
2487 .iter()
2488 .map(|config| {
2489 let id = config.capability_id();
2490 let id = registry.canonical_id(id).unwrap_or(id);
2491 (id.to_string(), config.config.clone())
2492 })
2493 .collect();
2494
2495 Ok(resolved
2496 .resolved_ids
2497 .into_iter()
2498 .map(|capability_id| {
2499 explicit_configs
2500 .get(&capability_id)
2501 .cloned()
2502 .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
2503 .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
2504 })
2505 .collect())
2506}
2507
2508fn resolve_single_capability(
2510 cap_id: &str,
2511 registry: &CapabilityRegistry,
2512 resolved: &mut Vec<String>,
2513 resolved_set: &mut std::collections::HashSet<String>,
2514 added_as_dependencies: &mut Vec<String>,
2515 user_selected: &std::collections::HashSet<String>,
2516 visiting: &mut Vec<String>,
2517) -> Result<(), DependencyError> {
2518 let cap_id = registry.canonical_id(cap_id).unwrap_or(cap_id);
2522
2523 if resolved_set.contains(cap_id) {
2525 return Ok(());
2526 }
2527
2528 if visiting.contains(&cap_id.to_string()) {
2530 return Err(DependencyError::CircularDependency {
2531 capability_id: cap_id.to_string(),
2532 chain: visiting.clone(),
2533 });
2534 }
2535
2536 let capability = match registry.get(cap_id) {
2538 Some(cap) => cap,
2539 None => {
2540 if (is_declarative_capability(cap_id) || is_plugin_capability(cap_id))
2544 && !resolved_set.contains(cap_id)
2545 {
2546 resolved.push(cap_id.to_string());
2547 resolved_set.insert(cap_id.to_string());
2548 if !user_selected.contains(cap_id) {
2549 added_as_dependencies.push(cap_id.to_string());
2550 }
2551 }
2552 return Ok(());
2553 }
2554 };
2555
2556 visiting.push(cap_id.to_string());
2558
2559 for dep_id in capability.dependencies() {
2561 resolve_single_capability(
2562 dep_id,
2563 registry,
2564 resolved,
2565 resolved_set,
2566 added_as_dependencies,
2567 user_selected,
2568 visiting,
2569 )?;
2570 }
2571
2572 visiting.pop();
2574
2575 if !resolved_set.contains(cap_id) {
2577 resolved.push(cap_id.to_string());
2578 resolved_set.insert(cap_id.to_string());
2579
2580 if !user_selected.contains(cap_id) {
2582 added_as_dependencies.push(cap_id.to_string());
2583 }
2584 }
2585
2586 Ok(())
2587}
2588
2589pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
2594 use std::collections::HashSet;
2595
2596 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2597 Ok(resolved) => resolved.resolved_ids,
2598 Err(_) => capability_ids.to_vec(),
2599 };
2600
2601 let mut seen = HashSet::new();
2602 let mut features = Vec::new();
2603 for cap_id in &resolved_ids {
2604 if let Some(cap) = registry.get(cap_id) {
2605 for feature in cap.features() {
2606 if seen.insert(feature) {
2607 features.push(feature.to_string());
2608 }
2609 }
2610 }
2611 }
2612 features
2613}
2614
2615pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
2618 registry
2619 .get(cap_id)
2620 .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
2621 .unwrap_or_default()
2622}
2623
2624pub async fn collect_capabilities(
2640 capability_ids: &[String],
2641 registry: &CapabilityRegistry,
2642 ctx: &SystemPromptContext,
2643) -> CollectedCapabilities {
2644 let resolved_ids = match resolve_dependencies(capability_ids, registry) {
2647 Ok(resolved) => resolved.resolved_ids,
2648 Err(e) => {
2649 tracing::warn!("Failed to resolve capability dependencies: {}", e);
2650 capability_ids.to_vec()
2651 }
2652 };
2653
2654 let configs: Vec<AgentCapabilityConfig> = resolved_ids
2656 .iter()
2657 .map(|id| AgentCapabilityConfig {
2658 capability_ref: CapabilityId::new(id),
2659 config: serde_json::Value::Object(serde_json::Map::new()),
2660 })
2661 .collect();
2662
2663 collect_capabilities_with_configs(&configs, registry, ctx).await
2664}
2665
2666pub async fn collect_capabilities_with_configs(
2677 capability_configs: &[AgentCapabilityConfig],
2678 registry: &CapabilityRegistry,
2679 ctx: &SystemPromptContext,
2680) -> CollectedCapabilities {
2681 let mut system_prompt_parts: Vec<String> = Vec::new();
2682 let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
2683 let mut tools: Vec<Box<dyn Tool>> = Vec::new();
2684 let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
2685 let mut mounts: Vec<MountPoint> = Vec::new();
2686 let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
2687 Vec::new();
2688 let mut applied_ids: Vec<String> = Vec::new();
2689 let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
2690 let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
2691 let mut openrouter_routing: Option<crate::driver_registry::OpenRouterRoutingConfig> = None;
2692 let mut parallel_tool_calls: Option<bool> = None;
2693 let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
2694 let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2695 let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
2698 let mut mcp_servers = ScopedMcpServers::default();
2699 let mut static_facts: Vec<Fact> = Vec::new();
2703 let mut has_dynamic_facts = false;
2704 let facts_ctx = FactsContext::new(ctx.session_id);
2705 let compaction_on = compaction_is_enabled(capability_configs, registry);
2706 let mut agent_handoff_spawn_config: Option<serde_json::Value> = None;
2707 let mut spawn_agent_providers: Vec<SpawnAgentTargetProvider> = Vec::new();
2708
2709 for cap_config in capability_configs {
2710 let cap_id = cap_config.capability_ref.as_str();
2711 if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
2716 match serde_json::from_value::<DeclarativeCapabilityDefinition>(
2717 cap_config.config.clone(),
2718 ) {
2719 Ok(definition) => {
2720 if definition.status != CapabilityStatus::Available {
2721 continue;
2722 }
2723
2724 if let Some(prompt) = definition.system_prompt.as_deref() {
2725 let contribution =
2726 format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
2727 system_prompt_attributions.push(SystemPromptAttribution {
2728 capability_id: cap_id.to_string(),
2729 content: contribution.clone(),
2730 });
2731 system_prompt_parts.push(contribution);
2732 }
2733
2734 mounts.extend(definition.mounts(cap_id));
2735 if let Some(ref servers) = definition.mcp_servers {
2736 mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
2737 }
2738 for skill in definition.skill_contributions() {
2739 mounts.push(skill.to_mount(cap_id));
2740 }
2741
2742 applied_ids.push(cap_id.to_string());
2743 }
2744 Err(error) => {
2745 tracing::warn!(
2746 capability_id = %cap_id,
2747 error = %error,
2748 "Skipping invalid declarative/plugin capability config"
2749 );
2750 }
2751 }
2752 continue;
2753 }
2754 if let Some(capability) = registry.get(cap_id) {
2755 if capability.status() != CapabilityStatus::Available {
2757 continue;
2758 }
2759
2760 let effective: &dyn Capability =
2772 match capability.resolve_for_model(ctx.model.as_deref()) {
2773 Some(inner) => inner,
2774 None => capability.as_ref(),
2775 };
2776 let effective_id = effective.id();
2777 if cap_id == AGENT_HANDOFF_CAPABILITY_ID {
2778 agent_handoff_spawn_config = Some(cap_config.config.clone());
2779 }
2780
2781 if let Some(contribution) = effective
2783 .system_prompt_contribution_with_config(ctx, &cap_config.config)
2784 .await
2785 {
2786 system_prompt_attributions.push(SystemPromptAttribution {
2787 capability_id: cap_id.to_string(),
2788 content: contribution.clone(),
2789 });
2790 system_prompt_parts.push(contribution);
2791 }
2792
2793 for fact in effective.facts(&cap_config.config, &facts_ctx) {
2798 match fact.volatility {
2799 Volatility::Static => static_facts.push(fact),
2800 Volatility::Dynamic => has_dynamic_facts = true,
2801 }
2802 }
2803
2804 for tool in effective.tools_with_config(&cap_config.config) {
2806 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && tool.name() == "spawn_agent" {
2807 spawn_agent_providers.push(SpawnAgentTargetProvider {
2808 target_type: "external_a2a",
2809 tool,
2810 });
2811 } else {
2812 tools.push(tool);
2813 }
2814 }
2815 tool_definition_hooks
2816 .extend(effective.tool_definition_hooks_with_context(ctx, &cap_config.config));
2817 tool_call_hooks.extend(effective.tool_call_hooks());
2818 narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
2820 let cap_category = effective.category();
2825 for def in effective.tool_definitions() {
2826 if cap_id == A2A_AGENT_DELEGATION_CAPABILITY_ID && def.name() == "spawn_agent" {
2827 continue;
2828 }
2829 let def = match (def.category(), cap_category) {
2830 (None, Some(cat)) => def.with_category(cat),
2831 _ => def,
2832 }
2833 .with_capability_attribution(cap_id, Some(capability.name()));
2834 tool_definitions.push(def);
2835 }
2836
2837 if effective_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID
2845 || effective_id == CLAUDE_TOOL_SEARCH_CAPABILITY_ID
2846 {
2847 let threshold = cap_config
2849 .config
2850 .get("threshold")
2851 .and_then(|v| v.as_u64())
2852 .map(|v| v as usize)
2853 .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
2854 tool_search = Some(crate::driver_registry::ToolSearchConfig {
2855 enabled: true,
2856 threshold,
2857 });
2858 }
2859
2860 if cap_id == PROMPT_CACHING_CAPABILITY_ID {
2861 let strategy = cap_config
2862 .config
2863 .get("strategy")
2864 .and_then(|v| v.as_str())
2865 .map(|value| match value {
2866 "auto" => crate::driver_registry::PromptCacheStrategy::Auto,
2867 _ => crate::driver_registry::PromptCacheStrategy::Auto,
2868 })
2869 .unwrap_or(crate::driver_registry::PromptCacheStrategy::Auto);
2870 let gemini_cached_content = cap_config
2871 .config
2872 .get("gemini_cached_content")
2873 .and_then(|v| v.as_str())
2874 .map(str::to_string);
2875 prompt_cache = Some(crate::driver_registry::PromptCacheConfig {
2876 enabled: true,
2877 strategy,
2878 gemini_cached_content,
2879 });
2880 }
2881
2882 if cap_id == PARALLEL_TOOL_CALLS_CAPABILITY_ID {
2883 parallel_tool_calls =
2884 parallel_tool_calls::parallel_tool_calls_from_config(&cap_config.config);
2885 }
2886
2887 if cap_id == OPENROUTER_SERVER_TOOLS_CAPABILITY_ID {
2888 let server_tools =
2889 openrouter_server_tools::server_tools_from_config(&cap_config.config);
2890 if !server_tools.is_empty() {
2891 openrouter_routing = Some(crate::driver_registry::OpenRouterRoutingConfig {
2892 server_tools,
2893 ..Default::default()
2894 });
2895 }
2896 }
2897
2898 mounts.extend(effective.mounts());
2900
2901 mcp_servers = merge_scoped_mcp_servers(
2902 &mcp_servers,
2903 &effective.mcp_servers_with_config(&cap_config.config),
2904 );
2905
2906 for skill in effective.contribute_skills() {
2910 mounts.push(skill.to_mount(cap_id));
2911 }
2912
2913 if let Some(provider) = effective.message_filter_provider() {
2915 let config = message_filter_config_for(cap_id, &cap_config.config, compaction_on);
2916 message_filter_providers.push((provider, config));
2917 }
2918
2919 applied_ids.push(cap_id.to_string());
2920 }
2921 }
2922
2923 if applied_ids.iter().any(|id| id == SUBAGENTS_CAPABILITY_ID) {
2928 spawn_agent_providers.push(SpawnAgentTargetProvider {
2929 target_type: "subagent",
2930 tool: Box::new(SpawnSubagentAsAgentTool),
2931 });
2932 }
2933 if let Some(config) = agent_handoff_spawn_config.as_ref() {
2934 spawn_agent_providers.push(SpawnAgentTargetProvider {
2935 target_type: "agent",
2936 tool: Box::new(SpawnAgentHandoffTool::new(config)),
2937 });
2938 }
2939 if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !spawn_agent_providers.is_empty() {
2940 let tool = UnifiedSpawnAgentTool::new(spawn_agent_providers);
2941 let def = tool
2942 .to_definition()
2943 .with_category("Orchestration")
2944 .with_capability_attribution("agent_delegation", Some("Agent Delegation"));
2945 tools.push(Box::new(tool));
2946 tool_definitions.push(def);
2947 }
2948
2949 if !applied_ids
2961 .iter()
2962 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
2963 && tool_definitions
2964 .iter()
2965 .any(|def| def.hints().supports_background == Some(true))
2966 && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
2967 && bg_cap.status() == CapabilityStatus::Available
2968 {
2969 tools.extend(bg_cap.tools());
2970 let cap_category = bg_cap.category();
2971 for def in bg_cap.tool_definitions() {
2972 let def = match (def.category(), cap_category) {
2973 (None, Some(cat)) => def.with_category(cat),
2974 _ => def,
2975 }
2976 .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
2977 tool_definitions.push(def);
2978 }
2979 narration_hooks.push(Arc::new(CapabilityNarrationHook(bg_cap.clone())));
2980 applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
2981 }
2982
2983 if let Some(block) = facts::render_facts_block(&static_facts) {
2988 system_prompt_attributions.push(SystemPromptAttribution {
2989 capability_id: "facts".to_string(),
2990 content: block.clone(),
2991 });
2992 system_prompt_parts.push(block);
2993 }
2994 if has_dynamic_facts {
2995 system_prompt_attributions.push(SystemPromptAttribution {
2996 capability_id: "facts".to_string(),
2997 content: FACTS_DYNAMIC_NOTE.to_string(),
2998 });
2999 system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
3000 }
3001
3002 tool_call_hooks.extend(narration_hooks);
3006
3007 message_filter_providers.sort_by_key(|(p, _)| p.priority());
3009
3010 CollectedCapabilities {
3011 system_prompt_parts,
3012 system_prompt_attributions,
3013 tools,
3014 tool_definitions,
3015 mounts,
3016 message_filter_providers,
3017 applied_ids,
3018 tool_search,
3019 prompt_cache,
3020 openrouter_routing,
3021 parallel_tool_calls,
3022 tool_definition_hooks,
3023 tool_call_hooks,
3024 mcp_servers,
3025 }
3026}
3027
3028pub struct AppliedCapabilities {
3034 pub runtime_agent: RuntimeAgent,
3036 pub tool_registry: ToolRegistry,
3038 pub applied_ids: Vec<String>,
3040}
3041
3042pub async fn apply_capabilities(
3079 base_runtime_agent: RuntimeAgent,
3080 capability_ids: &[String],
3081 registry: &CapabilityRegistry,
3082 ctx: &SystemPromptContext,
3083) -> AppliedCapabilities {
3084 let collected = collect_capabilities(capability_ids, registry, ctx).await;
3085
3086 let final_system_prompt = compose_system_prompt(
3088 &base_runtime_agent.system_prompt,
3089 collected.system_prompt_prefix().as_deref(),
3090 );
3091
3092 let mut tool_registry = ToolRegistry::new();
3094 for tool in collected.tools {
3095 tool_registry.register_boxed(tool);
3096 }
3097
3098 let mut tools = collected.tool_definitions;
3100 for hook in &collected.tool_definition_hooks {
3101 tools = hook.transform(tools);
3102 }
3103
3104 let runtime_agent = RuntimeAgent {
3105 system_prompt: final_system_prompt,
3106 model: base_runtime_agent.model,
3107 tools,
3108 max_iterations: base_runtime_agent.max_iterations,
3109 temperature: base_runtime_agent.temperature,
3110 max_tokens: base_runtime_agent.max_tokens,
3111 tool_search: collected.tool_search,
3112 prompt_cache: collected.prompt_cache,
3113 openrouter_routing: collected.openrouter_routing,
3114 network_access: base_runtime_agent.network_access,
3115 parallel_tool_calls: base_runtime_agent
3118 .parallel_tool_calls
3119 .or(collected.parallel_tool_calls),
3120 };
3121
3122 AppliedCapabilities {
3123 runtime_agent,
3124 tool_registry,
3125 applied_ids: collected.applied_ids,
3126 }
3127}
3128
3129#[cfg(test)]
3134mod tests {
3135 use super::*;
3136 use crate::typed_id::SessionId;
3137 use std::collections::BTreeSet;
3138 use uuid::Uuid;
3139
3140 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3142
3143 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
3144 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
3145 }
3146
3147 fn test_ctx() -> SystemPromptContext {
3149 SystemPromptContext::without_file_store(SessionId::new())
3150 }
3151
3152 fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
3154 let mut ids = [
3155 "agent_instructions",
3156 "human_intent",
3157 "budgeting",
3158 "self_budget",
3159 "noop",
3160 "current_time",
3161 "research",
3162 "platform_management",
3163 "session_file_system",
3164 "session_storage",
3165 "session",
3166 "session_sql_database",
3167 "test_math",
3168 "test_weather",
3169 "stateless_todo_list",
3170 "web_fetch",
3171 "bashkit_shell",
3172 "background_execution",
3173 "session_schedule",
3174 "btw",
3175 "infinity_context",
3176 "compaction",
3177 "memory",
3178 "message_metadata",
3179 "openai_tool_search",
3180 "claude_tool_search",
3181 "tool_search",
3182 "auto_tool_search",
3183 "prompt_caching",
3184 "parallel_tool_calls",
3185 "session_tasks",
3186 "skills",
3187 "subagents",
3188 "system_commands",
3189 "sample_data",
3190 "data_knowledge",
3191 "knowledge_base",
3192 "knowledge_index",
3193 "tool_output_persistence",
3194 "tool_output_distillation",
3195 "fake_warehouse",
3196 "fake_aws",
3197 "fake_crm",
3198 "fake_financial",
3199 "loop_detection",
3200 "usage_limit_auto_continue",
3201 "tool_call_repair",
3202 "error_disclosure",
3203 "prompt_canary_guardrail",
3204 "guardrails",
3205 "user_hooks",
3206 "model_scout",
3207 "openrouter_workspace",
3208 "openrouter_server_tools",
3209 ]
3210 .into_iter()
3211 .collect::<BTreeSet<_>>();
3212 if cfg!(feature = "ui-capabilities") {
3213 ids.insert("openui");
3214 ids.insert("a2ui");
3215 }
3216 ids
3217 }
3218
3219 fn expected_runtime_builtin_ids() -> BTreeSet<&'static str> {
3221 let mut ids = [
3222 "agent_instructions",
3223 "human_intent",
3224 "budgeting",
3225 "self_budget",
3226 "noop",
3227 "current_time",
3228 "session_file_system",
3229 "session_storage",
3230 "session",
3231 "stateless_todo_list",
3232 "bashkit_shell",
3233 "btw",
3234 "infinity_context",
3235 "compaction",
3236 "message_metadata",
3237 "openai_tool_search",
3238 "claude_tool_search",
3239 "tool_search",
3240 "auto_tool_search",
3241 "prompt_caching",
3242 "parallel_tool_calls",
3243 "skills",
3244 "system_commands",
3245 "tool_output_persistence",
3246 "tool_output_distillation",
3247 "loop_detection",
3248 "tool_call_repair",
3249 "error_disclosure",
3250 "prompt_canary_guardrail",
3251 "guardrails",
3252 "user_hooks",
3253 ]
3254 .into_iter()
3255 .collect::<BTreeSet<_>>();
3256 if cfg!(feature = "web-fetch") {
3257 ids.insert("web_fetch");
3258 }
3259 ids
3260 }
3261
3262 fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
3264 let mut ids = expected_core_builtin_ids();
3265 ids.insert("agent_handoff");
3266 ids.insert("a2a_agent_delegation");
3267 ids
3268 }
3269
3270 fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
3271 registry.capabilities.keys().map(String::as_str).collect()
3272 }
3273
3274 #[test]
3284 fn test_capability_registry_with_builtins_dev() {
3285 let _lock = lock_env();
3287 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3288 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3289 assert_eq!(registry_ids(®istry), expected_dev_builtin_ids());
3290 assert!(registry.has("agent_handoff"));
3291 assert!(registry.has("a2a_agent_delegation"));
3292 }
3293
3294 #[test]
3295 fn test_capability_registry_with_builtins_prod() {
3296 let _lock = lock_env();
3298 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3299 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3300 assert_eq!(registry_ids(®istry), expected_core_builtin_ids());
3301 assert!(!registry.has("docker_container"));
3303 assert!(!registry.has("agent_handoff"));
3304 assert!(!registry.has("a2a_agent_delegation"));
3305 }
3306
3307 #[test]
3308 fn test_capability_registry_runtime_builtins() {
3309 let _lock = lock_env();
3310 unsafe { std::env::remove_var("FEATURE_LUA") };
3311 let registry = CapabilityRegistry::runtime_builtins();
3312 assert_eq!(registry_ids(®istry), expected_runtime_builtin_ids());
3313 assert!(registry.has("session_file_system"));
3314 #[cfg(feature = "web-fetch")]
3315 assert!(registry.has("web_fetch"));
3316 assert!(registry.has("bashkit_shell"));
3317
3318 for platform_only in [
3319 "platform_management",
3320 "model_scout",
3321 "openrouter_workspace",
3322 "openrouter_server_tools",
3323 "session_tasks",
3324 "session_schedule",
3325 "subagents",
3326 "background_execution",
3327 "session_sql_database",
3328 "knowledge_base",
3329 "knowledge_index",
3330 "sample_data",
3331 "data_knowledge",
3332 "fake_aws",
3333 "fake_crm",
3334 "fake_financial",
3335 "fake_warehouse",
3336 "test_math",
3337 "test_weather",
3338 "research",
3339 ] {
3340 assert!(
3341 !registry.has(platform_only),
3342 "`{platform_only}` should not be in the runtime default registry"
3343 );
3344 }
3345 }
3346
3347 #[test]
3348 fn test_agent_delegation_enabled_by_env_in_prod() {
3349 let _lock = lock_env();
3351 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
3352 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3353 assert!(registry.has("agent_handoff"));
3354 assert!(registry.has("a2a_agent_delegation"));
3355 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3356 }
3357
3358 #[test]
3359 fn test_agent_delegation_disabled_by_env_in_dev() {
3360 let _lock = lock_env();
3362 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
3363 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
3364 assert!(!registry.has("agent_handoff"));
3365 assert!(!registry.has("a2a_agent_delegation"));
3366 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
3367 }
3368
3369 #[test]
3370 fn test_capability_registry_get() {
3371 let registry = CapabilityRegistry::with_builtins();
3372
3373 let noop = registry.get("noop").unwrap();
3374 assert_eq!(noop.id(), "noop");
3375 assert_eq!(noop.name(), "No-Op");
3376 assert_eq!(noop.status(), CapabilityStatus::Available);
3377 }
3378
3379 #[test]
3387 fn builtin_capabilities_satisfy_registry_invariants() {
3388 let registry = CapabilityRegistry::with_builtins();
3389
3390 for cap in registry.list() {
3391 let id = cap.id();
3392 assert!(!id.is_empty(), "capability has an empty id");
3393 assert!(
3394 !cap.name().trim().is_empty(),
3395 "capability `{id}` has an empty name"
3396 );
3397
3398 assert!(
3401 registry.get(id).is_some(),
3402 "capability `{id}` does not resolve by its own id"
3403 );
3404
3405 for dep in cap.dependencies() {
3409 assert!(
3410 registry.get(dep).is_some(),
3411 "capability `{id}` depends on `{dep}`, which is not registered"
3412 );
3413 }
3414
3415 let mut seen = std::collections::HashSet::new();
3418 for tool in cap.tools() {
3419 let name = tool.name().to_string();
3420 assert!(
3421 !name.is_empty(),
3422 "capability `{id}` exposes a tool with an empty name"
3423 );
3424 assert!(
3425 seen.insert(name.clone()),
3426 "capability `{id}` exposes duplicate tool name `{name}`"
3427 );
3428 }
3429
3430 let mut def_seen = std::collections::HashSet::new();
3433 for def in cap.tool_definitions() {
3434 let name = def.name().to_string();
3435 assert!(
3436 !name.is_empty(),
3437 "capability `{id}` advertises a tool definition with an empty name"
3438 );
3439 assert!(
3440 def_seen.insert(name.clone()),
3441 "capability `{id}` advertises duplicate tool definition name `{name}`"
3442 );
3443 }
3444 }
3445 }
3446
3447 #[test]
3448 fn test_capability_registry_blueprint_with_capability() {
3449 struct BlueprintProviderCapability;
3450
3451 impl Capability for BlueprintProviderCapability {
3452 fn id(&self) -> &str {
3453 "blueprint_provider"
3454 }
3455 fn name(&self) -> &str {
3456 "Blueprint Provider"
3457 }
3458 fn description(&self) -> &str {
3459 "Capability that provides a blueprint for tests"
3460 }
3461 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3462 vec![AgentBlueprint {
3463 id: "test_blueprint",
3464 name: "Test Blueprint",
3465 description: "Blueprint for capability registry tests",
3466 model: BlueprintModel::Inherit,
3467 system_prompt: "Test prompt",
3468 tools: vec![],
3469 max_turns: None,
3470 config_schema: None,
3471 }]
3472 }
3473 }
3474
3475 let mut registry = CapabilityRegistry::new();
3476 registry.register(BlueprintProviderCapability);
3477
3478 let (capability_id, blueprint) = registry
3479 .blueprint_with_capability("test_blueprint")
3480 .expect("blueprint should resolve with capability id");
3481 assert_eq!(capability_id, "blueprint_provider");
3482 assert_eq!(blueprint.id, "test_blueprint");
3483 }
3484
3485 #[test]
3486 fn test_capability_registry_builder() {
3487 let registry = CapabilityRegistry::builder()
3488 .capability(NoopCapability)
3489 .capability(CurrentTimeCapability)
3490 .build();
3491
3492 assert!(registry.has("noop"));
3493 assert!(registry.has("current_time"));
3494 assert_eq!(registry.len(), 2);
3495 }
3496
3497 #[test]
3498 fn test_capability_status() {
3499 let registry = CapabilityRegistry::with_builtins();
3500
3501 let current_time = registry.get("current_time").unwrap();
3502 assert_eq!(current_time.status(), CapabilityStatus::Available);
3503
3504 let research = registry.get("research").unwrap();
3505 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3506 }
3507
3508 #[test]
3509 fn test_capability_icons_and_categories() {
3510 let registry = CapabilityRegistry::with_builtins();
3511
3512 let noop = registry.get("noop").unwrap();
3513 assert_eq!(noop.icon(), Some("circle-off"));
3514 assert_eq!(noop.category(), Some("Testing"));
3515
3516 let current_time = registry.get("current_time").unwrap();
3517 assert_eq!(current_time.icon(), Some("clock"));
3518 assert_eq!(current_time.category(), Some("Core"));
3519 }
3520
3521 #[test]
3522 fn test_system_prompt_preview_default_delegates_to_addition() {
3523 let registry = CapabilityRegistry::with_builtins();
3524
3525 let test_math = registry.get("test_math").unwrap();
3527 assert_eq!(
3528 test_math.system_prompt_preview().as_deref(),
3529 test_math.system_prompt_addition()
3530 );
3531
3532 let current_time = registry.get("current_time").unwrap();
3534 assert!(current_time.system_prompt_preview().is_none());
3535 assert!(current_time.system_prompt_addition().is_none());
3536 }
3537
3538 #[test]
3539 fn test_system_prompt_preview_dynamic_capability() {
3540 let registry = CapabilityRegistry::with_builtins();
3541 let cap = registry.get("agent_instructions").unwrap();
3542
3543 assert!(cap.system_prompt_addition().is_none());
3545 assert!(cap.system_prompt_preview().is_some());
3546 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3547 }
3548
3549 #[tokio::test]
3554 async fn test_apply_capabilities_empty() {
3555 let registry = CapabilityRegistry::with_builtins();
3556 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3557
3558 let applied =
3559 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3560
3561 assert_eq!(
3562 applied.runtime_agent.system_prompt,
3563 base_runtime_agent.system_prompt
3564 );
3565 assert!(applied.tool_registry.is_empty());
3566 assert!(applied.applied_ids.is_empty());
3567 }
3568
3569 #[tokio::test]
3570 async fn test_apply_capabilities_noop() {
3571 let registry = CapabilityRegistry::with_builtins();
3572 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3573
3574 let applied = apply_capabilities(
3575 base_runtime_agent.clone(),
3576 &["noop".to_string()],
3577 ®istry,
3578 &test_ctx(),
3579 )
3580 .await;
3581
3582 assert_eq!(
3584 applied.runtime_agent.system_prompt,
3585 base_runtime_agent.system_prompt
3586 );
3587 assert!(applied.tool_registry.is_empty());
3588 assert_eq!(applied.applied_ids, vec!["noop"]);
3589 }
3590
3591 #[tokio::test]
3592 async fn test_apply_capabilities_current_time() {
3593 let registry = CapabilityRegistry::with_builtins();
3594 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3595
3596 let applied = apply_capabilities(
3597 base_runtime_agent.clone(),
3598 &["current_time".to_string()],
3599 ®istry,
3600 &test_ctx(),
3601 )
3602 .await;
3603
3604 assert!(
3608 applied
3609 .runtime_agent
3610 .system_prompt
3611 .contains(FACTS_DYNAMIC_NOTE),
3612 "current_time should contribute the dynamic-facts note"
3613 );
3614 assert!(
3615 applied
3616 .runtime_agent
3617 .system_prompt
3618 .contains(&base_runtime_agent.system_prompt),
3619 "base prompt is preserved"
3620 );
3621 assert!(applied.tool_registry.has("get_current_time"));
3622 assert_eq!(applied.tool_registry.len(), 1);
3623 assert_eq!(applied.applied_ids, vec!["current_time"]);
3624 }
3625
3626 #[tokio::test]
3627 async fn test_apply_capabilities_skips_coming_soon() {
3628 let registry = CapabilityRegistry::with_builtins();
3629 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3630
3631 let applied = apply_capabilities(
3633 base_runtime_agent.clone(),
3634 &["research".to_string()],
3635 ®istry,
3636 &test_ctx(),
3637 )
3638 .await;
3639
3640 assert_eq!(
3642 applied.runtime_agent.system_prompt,
3643 base_runtime_agent.system_prompt
3644 );
3645 assert!(applied.applied_ids.is_empty()); }
3647
3648 #[tokio::test]
3649 async fn test_apply_capabilities_multiple() {
3650 let registry = CapabilityRegistry::with_builtins();
3651 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3652
3653 let applied = apply_capabilities(
3654 base_runtime_agent.clone(),
3655 &["noop".to_string(), "current_time".to_string()],
3656 ®istry,
3657 &test_ctx(),
3658 )
3659 .await;
3660
3661 assert!(applied.tool_registry.has("get_current_time"));
3662 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3663 }
3664
3665 #[tokio::test]
3666 async fn test_apply_capabilities_preserves_order() {
3667 let registry = CapabilityRegistry::with_builtins();
3668 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3669
3670 let applied = apply_capabilities(
3672 base_runtime_agent,
3673 &["current_time".to_string(), "noop".to_string()],
3674 ®istry,
3675 &test_ctx(),
3676 )
3677 .await;
3678
3679 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3680 }
3681
3682 #[tokio::test]
3683 async fn test_apply_capabilities_test_math() {
3684 let registry = CapabilityRegistry::with_builtins();
3685 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3686
3687 let applied = apply_capabilities(
3688 base_runtime_agent.clone(),
3689 &["test_math".to_string()],
3690 ®istry,
3691 &test_ctx(),
3692 )
3693 .await;
3694
3695 assert!(
3697 !applied
3698 .runtime_agent
3699 .system_prompt
3700 .contains("<capability id=\"test_math\">")
3701 );
3702 assert!(
3704 applied
3705 .runtime_agent
3706 .system_prompt
3707 .contains("You are a helpful assistant.")
3708 );
3709 assert!(applied.tool_registry.has("add"));
3710 assert!(applied.tool_registry.has("subtract"));
3711 assert!(applied.tool_registry.has("multiply"));
3712 assert!(applied.tool_registry.has("divide"));
3713 assert_eq!(applied.tool_registry.len(), 4);
3714 }
3715
3716 #[tokio::test]
3717 async fn test_apply_capabilities_test_weather() {
3718 let registry = CapabilityRegistry::with_builtins();
3719 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3720
3721 let applied = apply_capabilities(
3722 base_runtime_agent.clone(),
3723 &["test_weather".to_string()],
3724 ®istry,
3725 &test_ctx(),
3726 )
3727 .await;
3728
3729 assert!(
3731 !applied
3732 .runtime_agent
3733 .system_prompt
3734 .contains("<capability id=\"test_weather\">")
3735 );
3736 assert!(applied.tool_registry.has("get_weather"));
3737 assert!(applied.tool_registry.has("get_forecast"));
3738 assert_eq!(applied.tool_registry.len(), 2);
3739 }
3740
3741 #[tokio::test]
3742 async fn test_apply_capabilities_test_math_and_test_weather() {
3743 let registry = CapabilityRegistry::with_builtins();
3744 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3745
3746 let applied = apply_capabilities(
3747 base_runtime_agent.clone(),
3748 &["test_math".to_string(), "test_weather".to_string()],
3749 ®istry,
3750 &test_ctx(),
3751 )
3752 .await;
3753
3754 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3757 assert!(applied.tool_registry.has("get_weather"));
3758 }
3759
3760 #[tokio::test]
3761 async fn test_apply_capabilities_stateless_todo_list() {
3762 let registry = CapabilityRegistry::with_builtins();
3763 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3764
3765 let applied = apply_capabilities(
3766 base_runtime_agent.clone(),
3767 &["stateless_todo_list".to_string()],
3768 ®istry,
3769 &test_ctx(),
3770 )
3771 .await;
3772
3773 assert!(
3775 applied
3776 .runtime_agent
3777 .system_prompt
3778 .contains("Task Management")
3779 );
3780 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3781 assert!(applied.tool_registry.has("write_todos"));
3782 assert_eq!(applied.tool_registry.len(), 1);
3783 }
3784
3785 #[tokio::test]
3786 async fn test_apply_capabilities_web_fetch() {
3787 let registry = CapabilityRegistry::with_builtins();
3788 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3789
3790 let applied = apply_capabilities(
3791 base_runtime_agent.clone(),
3792 &["web_fetch".to_string()],
3793 ®istry,
3794 &test_ctx(),
3795 )
3796 .await;
3797
3798 assert!(
3800 applied
3801 .runtime_agent
3802 .system_prompt
3803 .contains(&base_runtime_agent.system_prompt)
3804 );
3805 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3806 assert!(applied.tool_registry.has("web_fetch"));
3807 assert_eq!(applied.tool_registry.len(), 1);
3808 }
3809
3810 #[tokio::test]
3815 async fn test_xml_tags_wrap_capability_prompts() {
3816 let registry = CapabilityRegistry::with_builtins();
3817 let collected =
3818 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3819 .await;
3820
3821 assert_eq!(collected.system_prompt_parts.len(), 1);
3822 let part = &collected.system_prompt_parts[0];
3823 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3824 assert!(part.ends_with("</capability>"));
3825 assert!(part.contains("Task Management"));
3826 }
3827
3828 #[tokio::test]
3829 async fn test_xml_tags_multiple_capabilities() {
3830 let registry = CapabilityRegistry::with_builtins();
3831 let collected = collect_capabilities(
3832 &[
3833 "stateless_todo_list".to_string(),
3834 "session_schedule".to_string(),
3835 ],
3836 ®istry,
3837 &test_ctx(),
3838 )
3839 .await;
3840
3841 assert_eq!(collected.system_prompt_parts.len(), 2);
3842 assert!(
3843 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
3844 );
3845 assert!(
3846 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
3847 );
3848
3849 let prefix = collected.system_prompt_prefix().unwrap();
3850 assert!(prefix.contains("</capability>\n\n<capability"));
3852 }
3853
3854 #[tokio::test]
3855 async fn test_xml_tags_system_prompt_wrapping() {
3856 let registry = CapabilityRegistry::with_builtins();
3857 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3858
3859 let applied = apply_capabilities(
3860 base,
3861 &["stateless_todo_list".to_string()],
3862 ®istry,
3863 &test_ctx(),
3864 )
3865 .await;
3866
3867 let prompt = &applied.runtime_agent.system_prompt;
3868 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3869 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
3871 assert!(prompt.contains("</capability>"));
3872 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
3874 }
3875
3876 #[tokio::test]
3877 async fn test_no_xml_wrapping_without_capabilities() {
3878 let registry = CapabilityRegistry::with_builtins();
3879 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3880
3881 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
3882
3883 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3885 assert!(
3886 !applied
3887 .runtime_agent
3888 .system_prompt
3889 .contains("<system-prompt>")
3890 );
3891 }
3892
3893 #[tokio::test]
3894 async fn test_no_xml_wrapping_for_noop_capability() {
3895 let registry = CapabilityRegistry::with_builtins();
3896 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
3897
3898 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
3900
3901 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
3902 assert!(
3903 !applied
3904 .runtime_agent
3905 .system_prompt
3906 .contains("<system-prompt>")
3907 );
3908 }
3909
3910 #[tokio::test]
3915 async fn test_collect_capabilities_includes_mounts() {
3916 let registry = CapabilityRegistry::with_builtins();
3917
3918 let collected =
3919 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
3920
3921 assert!(!collected.mounts.is_empty());
3922 assert_eq!(collected.mounts.len(), 1);
3923 assert_eq!(collected.mounts[0].path, "/samples");
3924 assert!(collected.mounts[0].is_readonly());
3925 }
3926
3927 #[tokio::test]
3928 async fn test_collect_capabilities_empty_mounts_by_default() {
3929 let registry = CapabilityRegistry::with_builtins();
3930
3931 let collected =
3933 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
3934
3935 assert!(collected.mounts.is_empty());
3936 }
3937
3938 #[tokio::test]
3939 async fn test_dynamic_facts_add_note_without_static_block() {
3940 let registry = CapabilityRegistry::with_builtins();
3944 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3945 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3946 let prompt = collected.system_prompt_parts.join("\n");
3947 assert!(
3948 prompt.contains(FACTS_DYNAMIC_NOTE),
3949 "dynamic-facts note should be in the cached prompt"
3950 );
3951 assert!(
3952 !prompt.contains("<facts>\n"),
3953 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
3954 );
3955 }
3956
3957 #[tokio::test]
3958 async fn test_static_facts_fold_into_prompt() {
3959 struct StaticFactCap;
3960 impl Capability for StaticFactCap {
3961 fn id(&self) -> &str {
3962 "test_static_fact"
3963 }
3964 fn name(&self) -> &str {
3965 "Static Fact"
3966 }
3967 fn description(&self) -> &str {
3968 "test"
3969 }
3970 fn status(&self) -> CapabilityStatus {
3971 CapabilityStatus::Available
3972 }
3973 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
3974 vec![Fact::stat("workspace_root", "/workspace")]
3975 }
3976 }
3977 let mut registry = CapabilityRegistry::new();
3978 registry.register(StaticFactCap);
3979 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
3980 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
3981 let prompt = collected.system_prompt_parts.join("\n");
3982 assert!(
3983 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
3984 "static fact should fold into the cached prompt; got: {prompt}"
3985 );
3986 assert!(
3987 !prompt.contains(FACTS_DYNAMIC_NOTE),
3988 "no dynamic note when only static facts exist"
3989 );
3990 }
3991
3992 #[test]
3993 fn test_collect_dynamic_facts_returns_current_time() {
3994 let registry = CapabilityRegistry::with_builtins();
3995 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
3996 let facts = collect_dynamic_facts(
3997 &configs,
3998 ®istry,
3999 None,
4000 &FactsContext::new(SessionId::new()),
4001 );
4002 assert_eq!(facts.len(), 1);
4003 assert_eq!(facts[0].key, "current_time");
4004 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4005 }
4006
4007 #[tokio::test]
4008 async fn test_collect_capabilities_combines_mounts() {
4009 let registry = CapabilityRegistry::with_builtins();
4010
4011 let collected = collect_capabilities(
4014 &["sample_data".to_string(), "current_time".to_string()],
4015 ®istry,
4016 &test_ctx(),
4017 )
4018 .await;
4019
4020 assert_eq!(collected.mounts.len(), 1);
4021 assert!(
4023 collected
4024 .applied_ids
4025 .iter()
4026 .any(|id| id == "session_file_system")
4027 );
4028 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4029 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4030 }
4031
4032 #[test]
4033 fn test_sample_data_capability() {
4034 let registry = CapabilityRegistry::with_builtins();
4035 let cap = registry.get("sample_data").unwrap();
4036
4037 assert_eq!(cap.id(), "sample_data");
4038 assert_eq!(cap.name(), "Sample Data");
4039 assert_eq!(cap.status(), CapabilityStatus::Available);
4040
4041 assert!(cap.system_prompt_addition().is_some());
4043 assert!(cap.tools().is_empty());
4044
4045 assert!(!cap.mounts().is_empty());
4047 }
4048
4049 #[test]
4054 fn test_resolve_dependencies_empty() {
4055 let registry = CapabilityRegistry::with_builtins();
4056
4057 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4058
4059 assert!(resolved.resolved_ids.is_empty());
4060 assert!(resolved.added_as_dependencies.is_empty());
4061 assert!(resolved.user_selected.is_empty());
4062 }
4063
4064 #[test]
4065 fn test_resolve_dependencies_no_deps() {
4066 let registry = CapabilityRegistry::with_builtins();
4067
4068 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4070
4071 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4072 assert!(resolved.added_as_dependencies.is_empty());
4073 }
4074
4075 #[test]
4076 fn test_resolve_dependencies_with_deps() {
4077 let registry = CapabilityRegistry::with_builtins();
4078
4079 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4081
4082 assert_eq!(resolved.resolved_ids.len(), 2);
4084 let fs_pos = resolved
4085 .resolved_ids
4086 .iter()
4087 .position(|id| id == "session_file_system")
4088 .unwrap();
4089 let sd_pos = resolved
4090 .resolved_ids
4091 .iter()
4092 .position(|id| id == "sample_data")
4093 .unwrap();
4094 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4095
4096 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4098 }
4099
4100 #[test]
4101 fn test_resolve_dependencies_already_selected() {
4102 let registry = CapabilityRegistry::with_builtins();
4103
4104 let resolved = resolve_dependencies(
4106 &["session_file_system".to_string(), "sample_data".to_string()],
4107 ®istry,
4108 )
4109 .unwrap();
4110
4111 assert_eq!(resolved.resolved_ids.len(), 2);
4112 assert!(resolved.added_as_dependencies.is_empty());
4114 }
4115
4116 #[test]
4117 fn test_resolve_dependencies_preserves_order() {
4118 let registry = CapabilityRegistry::with_builtins();
4119
4120 let resolved =
4122 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4123 .unwrap();
4124
4125 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4126 }
4127
4128 #[test]
4129 fn test_resolve_dependencies_unknown_capability() {
4130 let registry = CapabilityRegistry::with_builtins();
4131
4132 let resolved =
4134 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4135
4136 assert!(resolved.resolved_ids.is_empty());
4137 }
4138
4139 #[test]
4140 fn test_get_dependencies() {
4141 let registry = CapabilityRegistry::with_builtins();
4142
4143 let deps = get_dependencies("sample_data", ®istry);
4145 assert_eq!(deps, vec!["session_file_system"]);
4146
4147 let deps = get_dependencies("current_time", ®istry);
4149 assert!(deps.is_empty());
4150
4151 let deps = get_dependencies("unknown", ®istry);
4153 assert!(deps.is_empty());
4154 }
4155
4156 #[test]
4157 fn test_sample_data_has_dependency() {
4158 let registry = CapabilityRegistry::with_builtins();
4159 let cap = registry.get("sample_data").unwrap();
4160
4161 let deps = cap.dependencies();
4162 assert_eq!(deps.len(), 1);
4163 assert_eq!(deps[0], "session_file_system");
4164 }
4165
4166 #[test]
4167 fn test_noop_has_no_dependencies() {
4168 let registry = CapabilityRegistry::with_builtins();
4169 let cap = registry.get("noop").unwrap();
4170
4171 assert!(cap.dependencies().is_empty());
4172 }
4173
4174 #[test]
4178 fn test_circular_dependency_error() {
4179 struct CapA;
4181 struct CapB;
4182
4183 impl Capability for CapA {
4184 fn id(&self) -> &str {
4185 "test_cap_a"
4186 }
4187 fn name(&self) -> &str {
4188 "Test A"
4189 }
4190 fn description(&self) -> &str {
4191 "Test capability A"
4192 }
4193 fn dependencies(&self) -> Vec<&'static str> {
4194 vec!["test_cap_b"]
4195 }
4196 }
4197
4198 impl Capability for CapB {
4199 fn id(&self) -> &str {
4200 "test_cap_b"
4201 }
4202 fn name(&self) -> &str {
4203 "Test B"
4204 }
4205 fn description(&self) -> &str {
4206 "Test capability B"
4207 }
4208 fn dependencies(&self) -> Vec<&'static str> {
4209 vec!["test_cap_a"]
4210 }
4211 }
4212
4213 let mut registry = CapabilityRegistry::new();
4214 registry.register(CapA);
4215 registry.register(CapB);
4216
4217 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4218
4219 assert!(result.is_err());
4220 match result.unwrap_err() {
4221 DependencyError::CircularDependency { capability_id, .. } => {
4222 assert_eq!(capability_id, "test_cap_a");
4223 }
4224 _ => panic!("Expected CircularDependency error"),
4225 }
4226 }
4227
4228 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4233
4234 struct FilterTestCapability {
4236 priority: i32,
4237 }
4238
4239 impl Capability for FilterTestCapability {
4240 fn id(&self) -> &str {
4241 "filter_test"
4242 }
4243 fn name(&self) -> &str {
4244 "Filter Test"
4245 }
4246 fn description(&self) -> &str {
4247 "Test capability with message filter"
4248 }
4249 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4250 Some(Arc::new(FilterTestProvider {
4251 priority: self.priority,
4252 }))
4253 }
4254 }
4255
4256 struct FilterTestProvider {
4257 priority: i32,
4258 }
4259
4260 impl MessageFilterProvider for FilterTestProvider {
4261 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4262 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4264 query
4265 .filters
4266 .push(MessageFilter::Search(search.to_string()));
4267 }
4268 }
4269
4270 fn priority(&self) -> i32 {
4271 self.priority
4272 }
4273 }
4274
4275 #[tokio::test]
4276 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4277 let registry = CapabilityRegistry::with_builtins();
4278 let configs = vec![AgentCapabilityConfig {
4279 capability_ref: CapabilityId::new("current_time"),
4280 config: serde_json::json!({}),
4281 }];
4282
4283 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4284
4285 assert!(collected.message_filter_providers.is_empty());
4286 assert!(!collected.has_message_filters());
4287 }
4288
4289 #[tokio::test]
4290 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4291 let mut registry = CapabilityRegistry::new();
4292 registry.register(FilterTestCapability { priority: 0 });
4293
4294 let configs = vec![AgentCapabilityConfig {
4295 capability_ref: CapabilityId::new("filter_test"),
4296 config: serde_json::json!({ "search": "hello" }),
4297 }];
4298
4299 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4300
4301 assert_eq!(collected.message_filter_providers.len(), 1);
4302 assert!(collected.has_message_filters());
4303 }
4304
4305 #[tokio::test]
4306 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4307 struct HighPriorityCapability;
4309 struct LowPriorityCapability;
4310
4311 impl Capability for HighPriorityCapability {
4312 fn id(&self) -> &str {
4313 "high_priority"
4314 }
4315 fn name(&self) -> &str {
4316 "High Priority"
4317 }
4318 fn description(&self) -> &str {
4319 "Test"
4320 }
4321 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4322 Some(Arc::new(FilterTestProvider { priority: 10 }))
4323 }
4324 }
4325
4326 impl Capability for LowPriorityCapability {
4327 fn id(&self) -> &str {
4328 "low_priority"
4329 }
4330 fn name(&self) -> &str {
4331 "Low Priority"
4332 }
4333 fn description(&self) -> &str {
4334 "Test"
4335 }
4336 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4337 Some(Arc::new(FilterTestProvider { priority: -5 }))
4338 }
4339 }
4340
4341 let mut registry = CapabilityRegistry::new();
4342 registry.register(HighPriorityCapability);
4343 registry.register(LowPriorityCapability);
4344
4345 let configs = vec![
4347 AgentCapabilityConfig {
4348 capability_ref: CapabilityId::new("high_priority"),
4349 config: serde_json::json!({}),
4350 },
4351 AgentCapabilityConfig {
4352 capability_ref: CapabilityId::new("low_priority"),
4353 config: serde_json::json!({}),
4354 },
4355 ];
4356
4357 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4358
4359 assert_eq!(collected.message_filter_providers.len(), 2);
4361 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4362 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4363 }
4364
4365 #[tokio::test]
4366 async fn test_collected_capabilities_apply_message_filters() {
4367 let mut registry = CapabilityRegistry::new();
4368 registry.register(FilterTestCapability { priority: 0 });
4369
4370 let configs = vec![AgentCapabilityConfig {
4371 capability_ref: CapabilityId::new("filter_test"),
4372 config: serde_json::json!({ "search": "test_query" }),
4373 }];
4374
4375 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4376
4377 let session_id: SessionId = Uuid::now_v7().into();
4379 let mut query = MessageQuery::new(session_id);
4380
4381 collected.apply_message_filters(&mut query);
4382
4383 assert_eq!(query.filters.len(), 1);
4385 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4386 }
4387
4388 #[tokio::test]
4389 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4390 struct SearchCapability {
4391 id: &'static str,
4392 search_term: &'static str,
4393 priority: i32,
4394 }
4395
4396 struct SearchProvider {
4397 search_term: &'static str,
4398 priority: i32,
4399 }
4400
4401 impl MessageFilterProvider for SearchProvider {
4402 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4403 query
4404 .filters
4405 .push(MessageFilter::Search(self.search_term.to_string()));
4406 }
4407
4408 fn priority(&self) -> i32 {
4409 self.priority
4410 }
4411 }
4412
4413 impl Capability for SearchCapability {
4414 fn id(&self) -> &str {
4415 self.id
4416 }
4417 fn name(&self) -> &str {
4418 "Search"
4419 }
4420 fn description(&self) -> &str {
4421 "Test"
4422 }
4423 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4424 Some(Arc::new(SearchProvider {
4425 search_term: self.search_term,
4426 priority: self.priority,
4427 }))
4428 }
4429 }
4430
4431 let mut registry = CapabilityRegistry::new();
4432 registry.register(SearchCapability {
4433 id: "cap_a",
4434 search_term: "alpha",
4435 priority: 5,
4436 });
4437 registry.register(SearchCapability {
4438 id: "cap_b",
4439 search_term: "beta",
4440 priority: 1,
4441 });
4442 registry.register(SearchCapability {
4443 id: "cap_c",
4444 search_term: "gamma",
4445 priority: 10,
4446 });
4447
4448 let configs = vec![
4449 AgentCapabilityConfig {
4450 capability_ref: CapabilityId::new("cap_a"),
4451 config: serde_json::json!({}),
4452 },
4453 AgentCapabilityConfig {
4454 capability_ref: CapabilityId::new("cap_b"),
4455 config: serde_json::json!({}),
4456 },
4457 AgentCapabilityConfig {
4458 capability_ref: CapabilityId::new("cap_c"),
4459 config: serde_json::json!({}),
4460 },
4461 ];
4462
4463 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4464
4465 let session_id: SessionId = Uuid::now_v7().into();
4466 let mut query = MessageQuery::new(session_id);
4467
4468 collected.apply_message_filters(&mut query);
4469
4470 assert_eq!(query.filters.len(), 3);
4472 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4473 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4474 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4475 }
4476
4477 #[test]
4478 fn test_capability_without_message_filter_returns_none() {
4479 let registry = CapabilityRegistry::with_builtins();
4480
4481 let noop = registry.get("noop").unwrap();
4482 assert!(noop.message_filter_provider().is_none());
4483
4484 let current_time = registry.get("current_time").unwrap();
4485 assert!(current_time.message_filter_provider().is_none());
4486 }
4487
4488 #[tokio::test]
4489 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4490 let mut registry = CapabilityRegistry::new();
4491 registry.register(FilterTestCapability { priority: 0 });
4492
4493 let test_config = serde_json::json!({
4494 "search": "custom_search",
4495 "extra_field": 42
4496 });
4497
4498 let configs = vec![AgentCapabilityConfig {
4499 capability_ref: CapabilityId::new("filter_test"),
4500 config: test_config.clone(),
4501 }];
4502
4503 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4504
4505 assert_eq!(collected.message_filter_providers.len(), 1);
4507 let (_, stored_config) = &collected.message_filter_providers[0];
4508 assert_eq!(*stored_config, test_config);
4509 }
4510
4511 #[test]
4516 fn test_collect_message_filters_only_collects_filters() {
4517 let mut registry = CapabilityRegistry::new();
4518 registry.register(FilterTestCapability { priority: 0 });
4519
4520 let configs = vec![AgentCapabilityConfig {
4521 capability_ref: CapabilityId::new("filter_test"),
4522 config: serde_json::json!({ "search": "test_query" }),
4523 }];
4524
4525 let collected = collect_message_filters_only(&configs, ®istry);
4526
4527 let session_id: SessionId = Uuid::now_v7().into();
4528 let mut query = MessageQuery::new(session_id);
4529 collected.apply_message_filters(&mut query);
4530
4531 assert_eq!(query.filters.len(), 1);
4532 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4533 }
4534
4535 #[test]
4536 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4537 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4538
4539 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4541 assert_eq!(with["compaction_active"], serde_json::json!(true));
4542 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4543
4544 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4545 assert!(without.get("compaction_active").is_none());
4546
4547 let other = message_filter_config_for("other", &base, true);
4549 assert!(other.get("compaction_active").is_none());
4550
4551 let null_base = message_filter_config_for(
4553 INFINITY_CONTEXT_CAPABILITY_ID,
4554 &serde_json::Value::Null,
4555 true,
4556 );
4557 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4558 }
4559
4560 #[test]
4561 fn test_infinity_context_defers_to_compaction_end_to_end() {
4562 use crate::message::Message;
4563
4564 let mut registry = CapabilityRegistry::new();
4565 registry.register(InfinityContextCapability);
4566 registry.register(CompactionCapability);
4567
4568 let tight = serde_json::json!({
4569 "context_budget_tokens": 1,
4570 "min_recent_messages": 1
4571 });
4572
4573 let solo = vec![AgentCapabilityConfig {
4575 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4576 config: tight.clone(),
4577 }];
4578 let mut messages = vec![
4579 Message::user("task"),
4580 Message::assistant("old ".repeat(400)),
4581 Message::user("recent"),
4582 ];
4583 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4584 assert!(
4585 messages
4586 .iter()
4587 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4588 "infinity context alone should trim and notice"
4589 );
4590
4591 let both = vec![
4593 AgentCapabilityConfig {
4594 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4595 config: tight,
4596 },
4597 AgentCapabilityConfig {
4598 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4599 config: serde_json::json!({}),
4600 },
4601 ];
4602 let mut messages = vec![
4603 Message::user("task"),
4604 Message::assistant("old ".repeat(400)),
4605 Message::user("recent"),
4606 ];
4607 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4608 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4609 assert!(
4610 messages
4611 .iter()
4612 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4613 "no hidden-history notice when compaction is the active reducer"
4614 );
4615 }
4616
4617 #[test]
4618 fn test_compaction_is_enabled_detects_compaction() {
4619 let mut registry = CapabilityRegistry::new();
4620 registry.register(CompactionCapability);
4621
4622 let with_compaction = vec![AgentCapabilityConfig {
4623 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4624 config: serde_json::json!({}),
4625 }];
4626 assert!(compaction_is_enabled(&with_compaction, ®istry));
4627
4628 let without = vec![AgentCapabilityConfig {
4629 capability_ref: CapabilityId::new("current_time"),
4630 config: serde_json::json!({}),
4631 }];
4632 assert!(!compaction_is_enabled(&without, ®istry));
4633 }
4634
4635 #[test]
4636 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4637 let registry = CapabilityRegistry::new();
4638
4639 let configs = vec![AgentCapabilityConfig {
4640 capability_ref: CapabilityId::new("nonexistent"),
4641 config: serde_json::json!({}),
4642 }];
4643
4644 let collected = collect_message_filters_only(&configs, ®istry);
4645 assert!(collected.message_filter_providers.is_empty());
4646 }
4647
4648 #[test]
4649 fn test_collect_message_filters_only_preserves_priority_order() {
4650 struct PriorityFilterCap {
4651 id: &'static str,
4652 search_term: &'static str,
4653 priority: i32,
4654 }
4655
4656 struct PriorityFilterProvider {
4657 search_term: &'static str,
4658 priority: i32,
4659 }
4660
4661 impl Capability for PriorityFilterCap {
4662 fn id(&self) -> &str {
4663 self.id
4664 }
4665 fn name(&self) -> &str {
4666 self.id
4667 }
4668 fn description(&self) -> &str {
4669 "priority test"
4670 }
4671 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4672 Some(Arc::new(PriorityFilterProvider {
4673 search_term: self.search_term,
4674 priority: self.priority,
4675 }))
4676 }
4677 }
4678
4679 impl MessageFilterProvider for PriorityFilterProvider {
4680 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4681 query
4682 .filters
4683 .push(MessageFilter::Search(self.search_term.to_string()));
4684 }
4685 fn priority(&self) -> i32 {
4686 self.priority
4687 }
4688 }
4689
4690 let mut registry = CapabilityRegistry::new();
4691 registry.register(PriorityFilterCap {
4692 id: "gamma",
4693 search_term: "gamma",
4694 priority: 10,
4695 });
4696 registry.register(PriorityFilterCap {
4697 id: "alpha",
4698 search_term: "alpha",
4699 priority: 5,
4700 });
4701 registry.register(PriorityFilterCap {
4702 id: "beta",
4703 search_term: "beta",
4704 priority: 1,
4705 });
4706
4707 let configs = vec![
4708 AgentCapabilityConfig {
4709 capability_ref: CapabilityId::new("gamma"),
4710 config: serde_json::json!({}),
4711 },
4712 AgentCapabilityConfig {
4713 capability_ref: CapabilityId::new("alpha"),
4714 config: serde_json::json!({}),
4715 },
4716 AgentCapabilityConfig {
4717 capability_ref: CapabilityId::new("beta"),
4718 config: serde_json::json!({}),
4719 },
4720 ];
4721
4722 let collected = collect_message_filters_only(&configs, ®istry);
4723
4724 let session_id: SessionId = Uuid::now_v7().into();
4725 let mut query = MessageQuery::new(session_id);
4726 collected.apply_message_filters(&mut query);
4727
4728 assert_eq!(query.filters.len(), 3);
4730 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4731 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4732 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4733 }
4734
4735 #[test]
4736 fn test_collect_message_filters_only_post_load_invoked() {
4737 use crate::message::Message;
4738
4739 struct PostLoadCap;
4740 struct PostLoadProvider;
4741
4742 impl Capability for PostLoadCap {
4743 fn id(&self) -> &str {
4744 "post_load_test"
4745 }
4746 fn name(&self) -> &str {
4747 "PostLoad Test"
4748 }
4749 fn description(&self) -> &str {
4750 "test"
4751 }
4752 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4753 Some(Arc::new(PostLoadProvider))
4754 }
4755 }
4756
4757 impl MessageFilterProvider for PostLoadProvider {
4758 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4759 fn priority(&self) -> i32 {
4760 0
4761 }
4762 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4763 messages.reverse();
4765 }
4766 }
4767
4768 let mut registry = CapabilityRegistry::new();
4769 registry.register(PostLoadCap);
4770
4771 let configs = vec![AgentCapabilityConfig {
4772 capability_ref: CapabilityId::new("post_load_test"),
4773 config: serde_json::json!({}),
4774 }];
4775
4776 let collected = collect_message_filters_only(&configs, ®istry);
4777
4778 let mut messages = vec![Message::user("first"), Message::user("second")];
4779 collected.apply_post_load_filters(&mut messages);
4780
4781 assert_eq!(messages[0].text(), Some("second"));
4783 assert_eq!(messages[1].text(), Some("first"));
4784 }
4785
4786 #[test]
4787 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4788 use crate::tool_types::ToolCall;
4789
4790 fn tool_heavy_messages() -> Vec<Message> {
4791 let mut messages = vec![Message::user("inspect files repeatedly")];
4792 for index in 0..9 {
4793 let call_id = format!("call_{index}");
4794 messages.push(Message::assistant_with_tools(
4795 "",
4796 vec![ToolCall {
4797 id: call_id.clone(),
4798 name: "read_file".to_string(),
4799 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4800 }],
4801 ));
4802 messages.push(Message::tool_result(
4803 call_id,
4804 Some(serde_json::json!({
4805 "path": "/workspace/src/lib.rs",
4806 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4807 "total_lines": 1000,
4808 "lines_shown": {"start": 1, "end": 1000},
4809 "truncated": false
4810 })),
4811 None,
4812 ));
4813 }
4814 messages
4815 }
4816
4817 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4818 messages[2]
4819 .tool_result_content()
4820 .and_then(|result| result.result.as_ref())
4821 .and_then(|result| result.get("masked"))
4822 .and_then(|masked| masked.as_bool())
4823 .unwrap_or(false)
4824 }
4825
4826 let mut registry = CapabilityRegistry::new();
4827 registry.register(CompactionCapability);
4828 let context = ModelViewContext {
4829 session_id: SessionId::new(),
4830 prior_usage: None,
4831 };
4832
4833 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4834 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4835 assert!(!first_tool_result_is_masked(&unmasked));
4836
4837 let compaction = collect_model_view_providers(
4838 &[AgentCapabilityConfig {
4839 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4840 config: serde_json::json!({}),
4841 }],
4842 ®istry,
4843 None,
4844 );
4845 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
4846 assert!(first_tool_result_is_masked(&masked));
4847 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
4848 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
4849 }
4850
4851 struct DelegatingFilterCap {
4854 id: &'static str,
4855 inner: std::sync::Arc<InnerFilterCap>,
4856 }
4857 struct InnerFilterCap;
4858
4859 impl Capability for InnerFilterCap {
4860 fn id(&self) -> &str {
4861 "inner_filter"
4862 }
4863 fn name(&self) -> &str {
4864 "Inner Filter"
4865 }
4866 fn description(&self) -> &str {
4867 "inner"
4868 }
4869 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4870 Some(std::sync::Arc::new(SentinelFilter))
4871 }
4872 }
4873 struct SentinelFilter;
4874 impl MessageFilterProvider for SentinelFilter {
4875 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4876 }
4877 impl Capability for DelegatingFilterCap {
4878 fn id(&self) -> &str {
4879 self.id
4880 }
4881 fn name(&self) -> &str {
4882 "Delegating Filter"
4883 }
4884 fn description(&self) -> &str {
4885 "delegating"
4886 }
4887 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
4888 None }
4890 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4891 Some(&*self.inner)
4892 }
4893 }
4894
4895 #[test]
4896 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
4897 let inner = std::sync::Arc::new(InnerFilterCap);
4898 let outer = DelegatingFilterCap {
4899 id: "delegating_filter",
4900 inner: inner.clone(),
4901 };
4902
4903 let mut registry = CapabilityRegistry::new();
4904 registry.register(outer);
4905
4906 let configs = vec![AgentCapabilityConfig {
4907 capability_ref: CapabilityId::new("delegating_filter"),
4908 config: serde_json::json!({}),
4909 }];
4910
4911 let collected = collect_message_filters_only(&configs, ®istry);
4914 assert_eq!(
4915 collected.message_filter_providers.len(),
4916 1,
4917 "provider from resolved inner capability must be collected"
4918 );
4919 }
4920
4921 struct DelegatingMvpCap {
4922 id: &'static str,
4923 inner: std::sync::Arc<InnerMvpCap>,
4924 }
4925 struct InnerMvpCap;
4926
4927 impl Capability for InnerMvpCap {
4928 fn id(&self) -> &str {
4929 "inner_mvp"
4930 }
4931 fn name(&self) -> &str {
4932 "Inner MVP"
4933 }
4934 fn description(&self) -> &str {
4935 "inner"
4936 }
4937 fn model_view_provider(
4938 &self,
4939 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4940 struct NoopMvp;
4942 impl crate::capabilities::ModelViewProvider for NoopMvp {
4943 fn apply_model_view(
4944 &self,
4945 messages: Vec<Message>,
4946 _config: &serde_json::Value,
4947 _context: &ModelViewContext<'_>,
4948 ) -> Vec<Message> {
4949 messages
4950 }
4951 }
4952 Some(std::sync::Arc::new(NoopMvp))
4953 }
4954 }
4955 impl Capability for DelegatingMvpCap {
4956 fn id(&self) -> &str {
4957 self.id
4958 }
4959 fn name(&self) -> &str {
4960 "Delegating MVP"
4961 }
4962 fn description(&self) -> &str {
4963 "delegating"
4964 }
4965 fn model_view_provider(
4966 &self,
4967 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
4968 None }
4970 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
4971 Some(&*self.inner)
4972 }
4973 }
4974
4975 #[test]
4976 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
4977 let inner = std::sync::Arc::new(InnerMvpCap);
4978 let outer = DelegatingMvpCap {
4979 id: "delegating_mvp",
4980 inner: inner.clone(),
4981 };
4982
4983 let mut registry = CapabilityRegistry::new();
4984 registry.register(outer);
4985
4986 let configs = vec![AgentCapabilityConfig {
4987 capability_ref: CapabilityId::new("delegating_mvp"),
4988 config: serde_json::json!({}),
4989 }];
4990
4991 let collected = collect_model_view_providers(&configs, ®istry, None);
4994 assert_eq!(
4995 collected.model_view_providers.len(),
4996 1,
4997 "provider from resolved inner capability must be collected"
4998 );
4999 }
5000
5001 #[tokio::test]
5011 async fn test_bashkit_shell_capability_produces_bash_tool() {
5012 let registry = CapabilityRegistry::with_builtins();
5013 let collected =
5014 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5015
5016 let tool_names: Vec<&str> = collected
5017 .tool_definitions
5018 .iter()
5019 .map(|t| t.name())
5020 .collect();
5021 assert!(
5022 tool_names.contains(&"bash"),
5023 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5024 tool_names
5025 );
5026 assert!(
5027 !collected.tools.is_empty(),
5028 "bashkit_shell must provide tool implementations"
5029 );
5030 }
5031
5032 #[tokio::test]
5033 async fn test_generic_harness_capability_set_produces_bash_tool() {
5034 let generic_harness_caps = vec![
5037 "session_file_system".to_string(),
5038 "bashkit_shell".to_string(),
5039 "web_fetch".to_string(),
5040 "session_storage".to_string(),
5041 "session".to_string(),
5042 "agent_instructions".to_string(),
5043 "skills".to_string(),
5044 "infinity_context".to_string(),
5045 "auto_tool_search".to_string(),
5046 ];
5047
5048 let registry = CapabilityRegistry::with_builtins();
5049 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5050
5051 let tool_names: Vec<&str> = collected
5052 .tool_definitions
5053 .iter()
5054 .map(|t| t.name())
5055 .collect();
5056 assert!(
5057 tool_names.contains(&"bash"),
5058 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5059 tool_names
5060 );
5061 }
5062
5063 #[tokio::test]
5064 async fn test_collect_capabilities_tool_count_matches_definitions() {
5065 let registry = CapabilityRegistry::with_builtins();
5068 let collected =
5069 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5070
5071 assert_eq!(
5072 collected.tools.len(),
5073 collected.tool_definitions.len(),
5074 "tool implementations ({}) must match tool definitions ({})",
5075 collected.tools.len(),
5076 collected.tool_definitions.len(),
5077 );
5078 }
5079
5080 #[tokio::test]
5084 async fn test_collect_capabilities_resolves_dependencies() {
5085 let registry = CapabilityRegistry::with_builtins();
5088 let collected =
5089 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5090
5091 assert!(
5093 collected
5094 .applied_ids
5095 .iter()
5096 .any(|id| id == "session_file_system"),
5097 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5098 collected.applied_ids
5099 );
5100
5101 let tool_names: Vec<&str> = collected
5102 .tool_definitions
5103 .iter()
5104 .map(|t| t.name())
5105 .collect();
5106
5107 assert!(
5109 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5110 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5111 tool_names
5112 );
5113
5114 assert_eq!(
5116 collected.tools.len(),
5117 collected.tool_definitions.len(),
5118 "dependency-added tools must have implementations, not just definitions"
5119 );
5120 }
5121
5122 #[test]
5123 fn test_defaults_do_not_include_bash() {
5124 let registry = crate::ToolRegistry::with_defaults();
5127 assert!(
5128 !registry.has("bash"),
5129 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5130 );
5131 }
5132
5133 #[tokio::test]
5140 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5141 let registry = CapabilityRegistry::with_builtins();
5142 let collected =
5143 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5144
5145 let tool_names: Vec<&str> = collected
5146 .tool_definitions
5147 .iter()
5148 .map(|t| t.name())
5149 .collect();
5150 assert!(
5151 tool_names.contains(&"spawn_background"),
5152 "spawn_background must be auto-activated when bashkit_shell (a \
5153 background-capable tool) is in the agent's capability set; got: {:?}",
5154 tool_names
5155 );
5156 assert!(
5157 collected
5158 .applied_ids
5159 .iter()
5160 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5161 "background_execution must be in applied_ids when auto-activated; \
5162 got: {:?}",
5163 collected.applied_ids
5164 );
5165
5166 assert!(
5168 collected
5169 .tools
5170 .iter()
5171 .any(|t| t.name() == "spawn_background"),
5172 "spawn_background tool implementation must be present alongside the \
5173 definition (lockstep contract)"
5174 );
5175 }
5176
5177 #[tokio::test]
5180 async fn test_background_execution_does_not_auto_activate_without_hint() {
5181 let registry = CapabilityRegistry::with_builtins();
5182 let collected =
5184 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5185
5186 let tool_names: Vec<&str> = collected
5187 .tool_definitions
5188 .iter()
5189 .map(|t| t.name())
5190 .collect();
5191 assert!(
5192 !tool_names.contains(&"spawn_background"),
5193 "spawn_background must NOT be activated without a background-capable \
5194 tool; got: {:?}",
5195 tool_names
5196 );
5197 assert!(
5198 !collected
5199 .applied_ids
5200 .iter()
5201 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5202 "background_execution must not appear in applied_ids when no \
5203 background-capable tool is present; got: {:?}",
5204 collected.applied_ids
5205 );
5206 }
5207
5208 #[tokio::test]
5209 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5210 let registry = CapabilityRegistry::with_builtins();
5211 let collected = collect_capabilities(
5212 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5213 ®istry,
5214 &test_ctx(),
5215 )
5216 .await;
5217
5218 assert!(
5219 collected
5220 .tools
5221 .iter()
5222 .any(|tool| tool.name() == "spawn_agent"),
5223 "subagent-only sessions should get the unified spawn_agent adapter"
5224 );
5225 let spawn_agent = collected
5226 .tool_definitions
5227 .iter()
5228 .find(|tool| tool.name() == "spawn_agent")
5229 .expect("spawn_agent definition");
5230 assert_eq!(
5231 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5232 serde_json::json!(["subagent"])
5233 );
5234 assert_eq!(
5235 spawn_agent.concurrency_class(),
5236 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5237 "unified spawn_agent must serialize same-batch spawns before cap checks"
5238 );
5239 }
5240
5241 #[tokio::test]
5242 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5243 let mut registry = CapabilityRegistry::new();
5244 registry.register(AgentHandoffCapability);
5245 let agent_id = crate::typed_id::AgentId::new();
5246 let harness_id = crate::typed_id::HarnessId::new();
5247 let configs = vec![AgentCapabilityConfig {
5248 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5249 config: serde_json::json!({
5250 "targets": [{
5251 "id": "aws_operator",
5252 "name": "AWS Operator",
5253 "agent_id": agent_id,
5254 "harness_id": harness_id
5255 }]
5256 }),
5257 }];
5258 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5259
5260 assert!(
5261 collected
5262 .tools
5263 .iter()
5264 .any(|tool| tool.name() == "spawn_agent"),
5265 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5266 );
5267 let spawn_agent = collected
5268 .tool_definitions
5269 .iter()
5270 .find(|tool| tool.name() == "spawn_agent")
5271 .expect("spawn_agent definition");
5272 assert_eq!(
5273 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5274 serde_json::json!(["agent"])
5275 );
5276 }
5277
5278 #[tokio::test]
5279 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5280 let mut registry = CapabilityRegistry::new();
5281 registry.register(SubagentCapability);
5282 registry.register(AgentHandoffCapability);
5283
5284 let agent_id = crate::typed_id::AgentId::new();
5285 let harness_id = crate::typed_id::HarnessId::new();
5286 let configs = vec![
5287 AgentCapabilityConfig {
5288 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5289 config: serde_json::json!({}),
5290 },
5291 AgentCapabilityConfig {
5292 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5293 config: serde_json::json!({
5294 "targets": [{
5295 "id": "aws_operator",
5296 "name": "AWS Operator",
5297 "agent_id": agent_id,
5298 "harness_id": harness_id
5299 }]
5300 }),
5301 },
5302 ];
5303
5304 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5305 let spawn_agent_defs: Vec<_> = collected
5306 .tool_definitions
5307 .iter()
5308 .filter(|tool| tool.name() == "spawn_agent")
5309 .collect();
5310
5311 assert_eq!(spawn_agent_defs.len(), 1);
5312 let schema = spawn_agent_defs[0].parameters();
5313 assert_eq!(
5314 schema["properties"]["target"]["properties"]["type"]["enum"],
5315 serde_json::json!(["subagent", "agent"])
5316 );
5317 assert!(schema.get("oneOf").is_none());
5320 assert!(schema.get("anyOf").is_none());
5321 assert!(schema.get("allOf").is_none());
5322 assert_eq!(
5323 schema["required"],
5324 serde_json::json!(["name", "instructions", "target"])
5325 );
5326 assert_eq!(
5327 schema["properties"]["target"]["oneOf"],
5328 serde_json::json!([
5329 {
5330 "properties": {"type": {"const": "subagent"}}
5331 },
5332 {
5333 "properties": {"type": {"const": "agent"}},
5334 "required": ["type", "id"]
5335 }
5336 ])
5337 );
5338 }
5339
5340 #[cfg(feature = "a2a")]
5341 #[tokio::test]
5342 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5343 let mut registry = CapabilityRegistry::new();
5344 registry.register(SubagentCapability);
5345 registry.register(A2aAgentDelegationCapability);
5346
5347 let configs = vec![
5348 AgentCapabilityConfig {
5349 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5350 config: serde_json::json!({}),
5351 },
5352 AgentCapabilityConfig {
5353 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5354 config: serde_json::json!({
5355 "agents": [{
5356 "id": "local_app",
5357 "name": "Local App",
5358 "base_url": "https://example.com"
5359 }]
5360 }),
5361 },
5362 ];
5363
5364 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5365 let spawn_agent_defs: Vec<_> = collected
5366 .tool_definitions
5367 .iter()
5368 .filter(|tool| tool.name() == "spawn_agent")
5369 .collect();
5370
5371 assert_eq!(spawn_agent_defs.len(), 1);
5372 assert_eq!(
5373 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5374 serde_json::json!(["subagent", "external_a2a"])
5375 );
5376 assert_eq!(
5377 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5378 serde_json::json!(["background", "foreground"])
5379 );
5380 assert!(
5381 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5382 .as_str()
5383 .expect("mode description")
5384 .contains("wait")
5385 );
5386 let schema = spawn_agent_defs[0].parameters();
5387 assert!(schema.get("oneOf").is_none());
5388 assert_eq!(
5392 schema["required"],
5393 serde_json::json!(["name", "instructions", "target"])
5394 );
5395 assert_eq!(
5396 schema["properties"]["target"]["oneOf"],
5397 serde_json::json!([
5398 {
5399 "properties": {"type": {"const": "subagent"}}
5400 },
5401 {
5402 "properties": {"type": {"const": "external_a2a"}},
5403 "anyOf": [
5404 {"required": ["id"]},
5405 {"required": ["external_agent_id"]}
5406 ]
5407 }
5408 ])
5409 );
5410 }
5411
5412 struct ExistingSpawnAgentCapability;
5413
5414 impl Capability for ExistingSpawnAgentCapability {
5415 fn id(&self) -> &str {
5416 "existing_spawn_agent"
5417 }
5418
5419 fn name(&self) -> &str {
5420 "Existing Spawn Agent"
5421 }
5422
5423 fn description(&self) -> &str {
5424 "Test capability that already owns spawn_agent"
5425 }
5426
5427 fn tools(&self) -> Vec<Box<dyn Tool>> {
5428 vec![Box::new(ExistingSpawnAgentTool)]
5429 }
5430 }
5431
5432 struct ExistingSpawnAgentTool;
5433
5434 #[async_trait]
5435 impl Tool for ExistingSpawnAgentTool {
5436 fn name(&self) -> &str {
5437 "spawn_agent"
5438 }
5439
5440 fn description(&self) -> &str {
5441 "Existing spawn_agent test tool"
5442 }
5443
5444 fn parameters_schema(&self) -> serde_json::Value {
5445 serde_json::json!({
5446 "type": "object",
5447 "properties": {
5448 "target": {
5449 "type": "object",
5450 "properties": {
5451 "type": {"type": "string", "enum": ["external_a2a"]}
5452 },
5453 "required": ["type"]
5454 }
5455 },
5456 "required": ["target"]
5457 })
5458 }
5459
5460 async fn execute(
5461 &self,
5462 _arguments: serde_json::Value,
5463 ) -> crate::tools::ToolExecutionResult {
5464 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5465 }
5466 }
5467
5468 #[tokio::test]
5469 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5470 let mut registry = CapabilityRegistry::new();
5471 registry.register(SubagentCapability);
5472 registry.register(ExistingSpawnAgentCapability);
5473
5474 let collected = collect_capabilities(
5475 &[
5476 SUBAGENTS_CAPABILITY_ID.to_string(),
5477 "existing_spawn_agent".to_string(),
5478 ],
5479 ®istry,
5480 &test_ctx(),
5481 )
5482 .await;
5483
5484 let spawn_agent_defs: Vec<_> = collected
5485 .tool_definitions
5486 .iter()
5487 .filter(|tool| tool.name() == "spawn_agent")
5488 .collect();
5489 assert_eq!(spawn_agent_defs.len(), 1);
5490 assert_eq!(
5491 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5492 serde_json::json!(["external_a2a"])
5493 );
5494 }
5495
5496 #[tokio::test]
5497 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5498 let mut registry = CapabilityRegistry::new();
5499 registry.register(AgentHandoffCapability);
5500 registry.register(ExistingSpawnAgentCapability);
5501
5502 let agent_id = crate::typed_id::AgentId::new();
5503 let harness_id = crate::typed_id::HarnessId::new();
5504 let configs = vec![
5505 AgentCapabilityConfig {
5506 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5507 config: serde_json::json!({
5508 "targets": [{
5509 "id": "aws_operator",
5510 "name": "AWS Operator",
5511 "agent_id": agent_id,
5512 "harness_id": harness_id
5513 }]
5514 }),
5515 },
5516 AgentCapabilityConfig {
5517 capability_ref: CapabilityId::new("existing_spawn_agent"),
5518 config: serde_json::json!({}),
5519 },
5520 ];
5521
5522 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5523
5524 let spawn_agent_defs: Vec<_> = collected
5525 .tool_definitions
5526 .iter()
5527 .filter(|tool| tool.name() == "spawn_agent")
5528 .collect();
5529 assert_eq!(spawn_agent_defs.len(), 1);
5530 assert_eq!(
5531 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5532 serde_json::json!(["external_a2a"])
5533 );
5534 }
5535
5536 #[tokio::test]
5540 async fn test_background_execution_explicit_selection_is_idempotent() {
5541 let registry = CapabilityRegistry::with_builtins();
5542 let collected = collect_capabilities(
5543 &[
5544 "bashkit_shell".to_string(),
5545 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5546 ],
5547 ®istry,
5548 &test_ctx(),
5549 )
5550 .await;
5551
5552 let spawn_background_count = collected
5553 .tool_definitions
5554 .iter()
5555 .filter(|t| t.name() == "spawn_background")
5556 .count();
5557 assert_eq!(
5558 spawn_background_count, 1,
5559 "spawn_background must appear exactly once even when \
5560 background_execution is selected explicitly alongside a \
5561 background-capable tool"
5562 );
5563 let applied_count = collected
5564 .applied_ids
5565 .iter()
5566 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5567 .count();
5568 assert_eq!(
5569 applied_count, 1,
5570 "background_execution must appear exactly once in applied_ids"
5571 );
5572 }
5573
5574 #[test]
5579 fn test_defaults_do_not_include_spawn_background() {
5580 let registry = crate::ToolRegistry::with_defaults();
5581 assert!(
5582 !registry.has("spawn_background"),
5583 "with_defaults() must not include 'spawn_background' — it comes \
5584 from the background_execution capability (EVE-501)"
5585 );
5586 }
5587
5588 #[test]
5593 fn test_capability_features_default_empty() {
5594 let registry = CapabilityRegistry::with_builtins();
5595
5596 let noop = registry.get("noop").unwrap();
5598 assert!(noop.features().is_empty());
5599
5600 let current_time = registry.get("current_time").unwrap();
5601 assert!(current_time.features().is_empty());
5602 }
5603
5604 #[test]
5605 fn test_file_system_capability_features() {
5606 let registry = CapabilityRegistry::with_builtins();
5607
5608 let fs = registry.get("session_file_system").unwrap();
5609 assert_eq!(fs.features(), vec!["file_system"]);
5610 }
5611
5612 #[test]
5613 fn test_bashkit_shell_capability_features() {
5614 let registry = CapabilityRegistry::with_builtins();
5615
5616 let bash = registry.get("bashkit_shell").unwrap();
5617 assert_eq!(bash.features(), vec!["file_system"]);
5618 }
5619
5620 #[test]
5621 fn test_alias_resolves_to_canonical_capability() {
5622 let registry = CapabilityRegistry::with_builtins();
5623
5624 let via_alias = registry.get("virtual_bash").unwrap();
5626 assert_eq!(via_alias.id(), "bashkit_shell");
5627 assert!(registry.has("virtual_bash"));
5628 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5629 assert_eq!(
5630 registry.canonical_id("bashkit_shell"),
5631 Some("bashkit_shell")
5632 );
5633 assert_eq!(registry.canonical_id("nonexistent"), None);
5634 }
5635
5636 #[test]
5637 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5638 let registry = CapabilityRegistry::with_builtins();
5639
5640 let resolved = resolve_dependencies(
5643 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5644 ®istry,
5645 )
5646 .unwrap();
5647 let bash_ids: Vec<_> = resolved
5648 .resolved_ids
5649 .iter()
5650 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5651 .collect();
5652 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5653 assert!(
5655 !resolved
5656 .added_as_dependencies
5657 .contains(&"bashkit_shell".to_string())
5658 );
5659 }
5660
5661 #[test]
5662 fn test_alias_preserves_explicit_config_in_resolution() {
5663 let registry = CapabilityRegistry::with_builtins();
5664
5665 let configs = vec![AgentCapabilityConfig::with_config(
5666 "virtual_bash".to_string(),
5667 serde_json::json!({"key": "value"}),
5668 )];
5669 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5670 let bash = resolved
5671 .iter()
5672 .find(|c| c.capability_id() == "bashkit_shell")
5673 .expect("alias must resolve to canonical bashkit_shell config");
5674 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5675 }
5676
5677 #[test]
5678 fn test_unregister_by_alias_removes_capability_and_aliases() {
5679 let mut registry = CapabilityRegistry::with_builtins();
5680
5681 assert!(registry.unregister("virtual_bash").is_some());
5682 assert!(!registry.has("bashkit_shell"));
5683 assert!(!registry.has("virtual_bash"));
5684 }
5685
5686 #[test]
5687 fn test_session_storage_capability_features() {
5688 let registry = CapabilityRegistry::with_builtins();
5689
5690 let storage = registry.get("session_storage").unwrap();
5691 let features = storage.features();
5692 assert!(features.contains(&"secrets"));
5693 assert!(features.contains(&"key_value"));
5694 }
5695
5696 #[test]
5697 fn test_session_schedule_capability_features() {
5698 let registry = CapabilityRegistry::with_builtins();
5699
5700 let schedule = registry.get("session_schedule").unwrap();
5701 assert_eq!(schedule.features(), vec!["schedules"]);
5702 }
5703
5704 #[test]
5705 fn test_session_sql_database_capability_features() {
5706 let registry = CapabilityRegistry::with_builtins();
5707
5708 let sql = registry.get("session_sql_database").unwrap();
5709 assert_eq!(sql.features(), vec!["sql_database"]);
5710 }
5711
5712 #[test]
5713 fn test_sample_data_capability_features() {
5714 let registry = CapabilityRegistry::with_builtins();
5715
5716 let sample = registry.get("sample_data").unwrap();
5717 assert_eq!(sample.features(), vec!["file_system"]);
5718 }
5719
5720 #[test]
5721 fn test_compute_features_empty() {
5722 let registry = CapabilityRegistry::with_builtins();
5723
5724 let features = compute_features(&[], ®istry);
5725 assert!(features.is_empty());
5726 }
5727
5728 #[test]
5729 fn test_compute_features_single_capability() {
5730 let registry = CapabilityRegistry::with_builtins();
5731
5732 let features = compute_features(&["session_schedule".to_string()], ®istry);
5733 assert_eq!(features, vec!["schedules"]);
5734 }
5735
5736 #[test]
5737 fn test_compute_features_multiple_capabilities() {
5738 let registry = CapabilityRegistry::with_builtins();
5739
5740 let features = compute_features(
5741 &[
5742 "session_file_system".to_string(),
5743 "session_storage".to_string(),
5744 "session_schedule".to_string(),
5745 ],
5746 ®istry,
5747 );
5748 assert!(features.contains(&"file_system".to_string()));
5749 assert!(features.contains(&"secrets".to_string()));
5750 assert!(features.contains(&"key_value".to_string()));
5751 assert!(features.contains(&"schedules".to_string()));
5752 }
5753
5754 #[test]
5755 fn test_compute_features_deduplicates() {
5756 let registry = CapabilityRegistry::with_builtins();
5757
5758 let features = compute_features(
5760 &[
5761 "session_file_system".to_string(),
5762 "bashkit_shell".to_string(),
5763 ],
5764 ®istry,
5765 );
5766 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5767 assert_eq!(file_system_count, 1, "file_system should appear only once");
5768 }
5769
5770 #[test]
5771 fn test_compute_features_includes_dependency_features() {
5772 let registry = CapabilityRegistry::with_builtins();
5773
5774 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5776 assert!(features.contains(&"file_system".to_string()));
5777 }
5778
5779 #[test]
5780 fn test_compute_features_generic_harness_set() {
5781 let registry = CapabilityRegistry::with_builtins();
5782
5783 let features = compute_features(
5785 &[
5786 "session_file_system".to_string(),
5787 "bashkit_shell".to_string(),
5788 "session_storage".to_string(),
5789 "session".to_string(),
5790 "session_schedule".to_string(),
5791 ],
5792 ®istry,
5793 );
5794 assert!(features.contains(&"file_system".to_string()));
5795 assert!(features.contains(&"secrets".to_string()));
5796 assert!(features.contains(&"key_value".to_string()));
5797 assert!(features.contains(&"schedules".to_string()));
5798 }
5799
5800 #[test]
5801 fn test_compute_features_unknown_capability_ignored() {
5802 let registry = CapabilityRegistry::with_builtins();
5803
5804 let features = compute_features(
5805 &["unknown_cap".to_string(), "session_schedule".to_string()],
5806 ®istry,
5807 );
5808 assert_eq!(features, vec!["schedules"]);
5809 }
5810
5811 #[test]
5812 fn test_risk_level_ordering() {
5813 assert!(RiskLevel::Low < RiskLevel::Medium);
5814 assert!(RiskLevel::Medium < RiskLevel::High);
5815 }
5816
5817 #[test]
5818 fn test_risk_level_serde_roundtrip() {
5819 let high = RiskLevel::High;
5820 let json = serde_json::to_string(&high).unwrap();
5821 assert_eq!(json, "\"high\"");
5822 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5823 assert_eq!(back, RiskLevel::High);
5824 }
5825
5826 #[test]
5827 fn test_capability_risk_levels() {
5828 let registry = CapabilityRegistry::with_builtins();
5829
5830 let bash = registry.get("bashkit_shell").unwrap();
5832 assert_eq!(bash.risk_level(), RiskLevel::High);
5833
5834 let fetch = registry.get("web_fetch").unwrap();
5836 assert_eq!(fetch.risk_level(), RiskLevel::High);
5837
5838 let noop = registry.get("noop").unwrap();
5840 assert_eq!(noop.risk_level(), RiskLevel::Low);
5841 }
5842
5843 #[tokio::test]
5848 async fn test_apply_capabilities_openai_tool_search() {
5849 let registry = CapabilityRegistry::with_builtins();
5850 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5851
5852 let applied = apply_capabilities(
5853 base_runtime_agent.clone(),
5854 &["openai_tool_search".to_string()],
5855 ®istry,
5856 &test_ctx(),
5857 )
5858 .await;
5859
5860 assert_eq!(
5862 applied.runtime_agent.system_prompt,
5863 base_runtime_agent.system_prompt
5864 );
5865 assert!(applied.tool_registry.is_empty());
5866 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
5867
5868 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5870 assert!(ts.enabled);
5871 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5872 }
5873
5874 #[tokio::test]
5875 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
5876 let registry = CapabilityRegistry::with_builtins();
5877 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
5878
5879 let applied = apply_capabilities(
5880 base_runtime_agent,
5881 &[
5882 "current_time".to_string(),
5883 "openai_tool_search".to_string(),
5884 "test_math".to_string(),
5885 ],
5886 ®istry,
5887 &test_ctx(),
5888 )
5889 .await;
5890
5891 assert!(applied.tool_registry.has("get_current_time"));
5893 assert!(applied.tool_registry.has("add"));
5894 assert!(applied.tool_registry.has("subtract"));
5895 assert!(applied.tool_registry.has("multiply"));
5896 assert!(applied.tool_registry.has("divide"));
5897
5898 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
5900 assert!(ts.enabled);
5901 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
5902 }
5903
5904 #[tokio::test]
5905 async fn test_collect_capabilities_tool_search_custom_threshold() {
5906 let registry = CapabilityRegistry::with_builtins();
5907
5908 let configs = vec![AgentCapabilityConfig {
5909 capability_ref: CapabilityId::new("openai_tool_search"),
5910 config: serde_json::json!({"threshold": 5}),
5911 }];
5912
5913 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5914
5915 let ts = collected.tool_search.as_ref().unwrap();
5916 assert!(ts.enabled);
5917 assert_eq!(ts.threshold, 5);
5918 }
5919
5920 #[tokio::test]
5921 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
5922 let registry = CapabilityRegistry::with_builtins();
5923
5924 let configs = vec![
5925 AgentCapabilityConfig {
5926 capability_ref: CapabilityId::new("auto_tool_search"),
5927 config: serde_json::json!({"threshold": 2}),
5928 },
5929 AgentCapabilityConfig {
5930 capability_ref: CapabilityId::new("test_math"),
5931 config: serde_json::json!({}),
5932 },
5933 ];
5934
5935 let ctx = test_ctx().with_model("claude-3-5-haiku");
5939 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5940
5941 assert!(
5942 collected.tool_search.is_none(),
5943 "auto_tool_search must not set a hosted config on a non-native model"
5944 );
5945 assert!(
5946 collected
5947 .tools
5948 .iter()
5949 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5950 "auto_tool_search must contribute the client-side tool_search tool"
5951 );
5952 assert!(
5953 !collected.tool_definition_hooks.is_empty(),
5954 "auto_tool_search must contribute a client-side deferral hook"
5955 );
5956
5957 let mut transformed = collected.tool_definitions.clone();
5958 for hook in &collected.tool_definition_hooks {
5959 transformed = hook.transform(transformed);
5960 }
5961 let add_tool = transformed
5962 .iter()
5963 .find(|tool| tool.name() == "add")
5964 .expect("test_math contributes add");
5965 assert!(
5966 add_tool.parameters().get("properties").is_none(),
5967 "generic auto_tool_search must honor the configured threshold"
5968 );
5969 }
5970
5971 #[tokio::test]
5972 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
5973 let registry = CapabilityRegistry::with_builtins();
5974
5975 let configs = vec![AgentCapabilityConfig {
5976 capability_ref: CapabilityId::new("auto_tool_search"),
5977 config: serde_json::json!({"threshold": 7}),
5978 }];
5979
5980 let ctx = test_ctx().with_model("gpt-5.4");
5983 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
5984
5985 let ts = collected
5986 .tool_search
5987 .as_ref()
5988 .expect("auto_tool_search must set a hosted config on a native model");
5989 assert!(ts.enabled);
5990 assert_eq!(ts.threshold, 7);
5991 assert!(
5992 !collected
5993 .tools
5994 .iter()
5995 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
5996 "hosted mechanism must not contribute the client-side tool_search tool"
5997 );
5998 assert!(
5999 collected.tool_definition_hooks.is_empty(),
6000 "hosted mechanism must not contribute a client-side deferral hook"
6001 );
6002 }
6003
6004 #[tokio::test]
6005 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6006 let registry = CapabilityRegistry::with_builtins();
6007
6008 let configs = vec![AgentCapabilityConfig {
6009 capability_ref: CapabilityId::new("auto_tool_search"),
6010 config: serde_json::json!({"threshold": 9}),
6011 }];
6012
6013 let ctx = test_ctx().with_model("claude-opus-4-8");
6016 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6017
6018 let ts = collected
6019 .tool_search
6020 .as_ref()
6021 .expect("auto_tool_search must set a hosted config on a native Claude model");
6022 assert!(ts.enabled);
6023 assert_eq!(ts.threshold, 9);
6024 assert!(
6025 !collected
6026 .tools
6027 .iter()
6028 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6029 "hosted mechanism must not contribute the client-side tool_search tool"
6030 );
6031 assert!(
6032 collected.tool_definition_hooks.is_empty(),
6033 "hosted mechanism must not contribute a client-side deferral hook"
6034 );
6035 }
6036
6037 #[tokio::test]
6038 async fn test_collect_capabilities_no_tool_search_without_capability() {
6039 let registry = CapabilityRegistry::with_builtins();
6040
6041 let configs = vec![AgentCapabilityConfig {
6042 capability_ref: CapabilityId::new("current_time"),
6043 config: serde_json::json!({}),
6044 }];
6045
6046 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6047
6048 assert!(collected.tool_search.is_none());
6049 }
6050
6051 #[tokio::test]
6052 async fn test_collect_capabilities_tool_search_category_propagation() {
6053 let registry = CapabilityRegistry::with_builtins();
6054
6055 let configs = vec![
6057 AgentCapabilityConfig {
6058 capability_ref: CapabilityId::new("test_math"),
6059 config: serde_json::json!({}),
6060 },
6061 AgentCapabilityConfig {
6062 capability_ref: CapabilityId::new("openai_tool_search"),
6063 config: serde_json::json!({}),
6064 },
6065 ];
6066
6067 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6068
6069 assert!(collected.tool_search.is_some());
6071
6072 for tool_def in &collected.tool_definitions {
6074 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6076 assert!(
6077 tool_def.category().is_some(),
6078 "Tool {} should have a category from its capability",
6079 tool_def.name()
6080 );
6081 }
6082 }
6083 }
6084
6085 #[tokio::test]
6086 async fn test_apply_capabilities_prompt_caching() {
6087 let registry = CapabilityRegistry::with_builtins();
6088 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6089
6090 let applied = apply_capabilities(
6091 base_runtime_agent.clone(),
6092 &["prompt_caching".to_string()],
6093 ®istry,
6094 &test_ctx(),
6095 )
6096 .await;
6097
6098 assert_eq!(
6099 applied.runtime_agent.system_prompt,
6100 base_runtime_agent.system_prompt
6101 );
6102 assert!(applied.tool_registry.is_empty());
6103 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6104
6105 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6106 assert!(prompt_cache.enabled);
6107 assert_eq!(
6108 prompt_cache.strategy,
6109 crate::driver_registry::PromptCacheStrategy::Auto
6110 );
6111 assert!(prompt_cache.gemini_cached_content.is_none());
6112 }
6113
6114 #[tokio::test]
6115 async fn test_apply_capabilities_openrouter_server_tools() {
6116 let registry = CapabilityRegistry::with_builtins();
6117 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6118
6119 let configs = vec![AgentCapabilityConfig {
6120 capability_ref: CapabilityId::new("openrouter_server_tools"),
6121 config: serde_json::json!({
6122 "tools": ["web_search", "datetime"],
6123 "web_search_max_results": 4,
6124 }),
6125 }];
6126
6127 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6128 let routing = collected
6129 .openrouter_routing
6130 .as_ref()
6131 .expect("server tools produce routing config");
6132 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6133 assert_eq!(
6134 kinds,
6135 vec![
6136 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6137 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6138 ]
6139 );
6140
6141 let applied = apply_capabilities(
6144 base_runtime_agent,
6145 &["openrouter_server_tools".to_string()],
6146 ®istry,
6147 &test_ctx(),
6148 )
6149 .await;
6150 assert!(applied.tool_registry.is_empty());
6151 assert!(applied.runtime_agent.openrouter_routing.is_none());
6152 }
6153
6154 #[tokio::test]
6155 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6156 let registry = CapabilityRegistry::with_builtins();
6157
6158 let configs = vec![AgentCapabilityConfig {
6159 capability_ref: CapabilityId::new("prompt_caching"),
6160 config: serde_json::json!({"strategy": "auto"}),
6161 }];
6162
6163 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6164
6165 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6166 assert!(prompt_cache.enabled);
6167 assert_eq!(
6168 prompt_cache.strategy,
6169 crate::driver_registry::PromptCacheStrategy::Auto
6170 );
6171 assert!(prompt_cache.gemini_cached_content.is_none());
6172 }
6173
6174 #[tokio::test]
6175 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6176 let registry = CapabilityRegistry::with_builtins();
6177
6178 let configs = vec![AgentCapabilityConfig {
6179 capability_ref: CapabilityId::new("prompt_caching"),
6180 config: serde_json::json!({
6181 "strategy": "auto",
6182 "gemini_cached_content": "cachedContents/demo-cache"
6183 }),
6184 }];
6185
6186 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6187
6188 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6189 assert_eq!(
6190 prompt_cache.gemini_cached_content.as_deref(),
6191 Some("cachedContents/demo-cache")
6192 );
6193 }
6194
6195 #[tokio::test]
6196 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6197 let registry = CapabilityRegistry::with_builtins();
6198
6199 let collected = collect_capabilities_with_configs(
6201 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6202 ®istry,
6203 &test_ctx(),
6204 )
6205 .await;
6206 assert_eq!(collected.parallel_tool_calls, Some(true));
6207
6208 let collected = collect_capabilities_with_configs(
6210 &[AgentCapabilityConfig {
6211 capability_ref: CapabilityId::new("parallel_tool_calls"),
6212 config: serde_json::json!({"mode": "avoid"}),
6213 }],
6214 ®istry,
6215 &test_ctx(),
6216 )
6217 .await;
6218 assert_eq!(collected.parallel_tool_calls, Some(false));
6219
6220 let collected = collect_capabilities_with_configs(
6222 &[AgentCapabilityConfig {
6223 capability_ref: CapabilityId::new("parallel_tool_calls"),
6224 config: serde_json::json!({"mode": "none"}),
6225 }],
6226 ®istry,
6227 &test_ctx(),
6228 )
6229 .await;
6230 assert_eq!(collected.parallel_tool_calls, None);
6231
6232 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6234 assert_eq!(collected.parallel_tool_calls, None);
6235 }
6236
6237 #[tokio::test]
6238 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6239 let registry = CapabilityRegistry::with_builtins();
6240
6241 let applied = apply_capabilities(
6243 RuntimeAgent::new("p", "gpt-5.2"),
6244 &["parallel_tool_calls".to_string()],
6245 ®istry,
6246 &test_ctx(),
6247 )
6248 .await;
6249 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6250
6251 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6253 base.parallel_tool_calls = Some(false);
6254 let applied = apply_capabilities(
6255 base,
6256 &["parallel_tool_calls".to_string()],
6257 ®istry,
6258 &test_ctx(),
6259 )
6260 .await;
6261 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6262 }
6263
6264 struct SkillContributingCapability;
6269
6270 impl Capability for SkillContributingCapability {
6271 fn id(&self) -> &str {
6272 "contributes_skills"
6273 }
6274 fn name(&self) -> &str {
6275 "Contributes Skills"
6276 }
6277 fn description(&self) -> &str {
6278 "Test capability that contributes skills."
6279 }
6280 fn contribute_skills(&self) -> Vec<SkillContribution> {
6281 vec![
6282 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6283 .with_files(vec![(
6284 "scripts/a.sh".to_string(),
6285 "#!/bin/sh\necho a\n".to_string(),
6286 )]),
6287 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6288 .with_user_invocable(false),
6289 ]
6290 }
6291 }
6292
6293 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6294 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6295 MountSource::InlineFile { content, .. } => content.as_str(),
6296 _ => panic!("Expected InlineFile for SKILL.md"),
6297 }
6298 }
6299
6300 #[tokio::test]
6301 async fn test_contribute_skills_normalized_to_mounts() {
6302 let mut registry = CapabilityRegistry::new();
6303 registry.register(SkillContributingCapability);
6304
6305 let configs = vec![AgentCapabilityConfig {
6306 capability_ref: CapabilityId::new("contributes_skills"),
6307 config: serde_json::json!({}),
6308 }];
6309
6310 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6311
6312 let skill_mounts: Vec<_> = collected
6313 .mounts
6314 .iter()
6315 .filter(|m| m.path.starts_with("/.agents/skills/"))
6316 .collect();
6317 assert_eq!(skill_mounts.len(), 2);
6318
6319 for m in &skill_mounts {
6322 assert!(m.is_readonly());
6323 assert_eq!(m.capability_id, "contributes_skills");
6324 }
6325
6326 let alpha = skill_mounts
6327 .iter()
6328 .find(|m| m.path == "/.agents/skills/alpha-skill")
6329 .expect("alpha-skill mount missing");
6330 match &alpha.source {
6331 MountSource::InlineDirectory { entries } => {
6332 assert!(entries.contains_key("SKILL.md"));
6333 assert!(entries.contains_key("scripts/a.sh"));
6334 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6335 assert_eq!(parsed.name, "alpha-skill");
6336 assert!(parsed.user_invocable);
6337 }
6338 _ => panic!("Expected InlineDirectory"),
6339 }
6340
6341 let beta = skill_mounts
6342 .iter()
6343 .find(|m| m.path == "/.agents/skills/beta-skill")
6344 .expect("beta-skill mount missing");
6345 match &beta.source {
6346 MountSource::InlineDirectory { entries } => {
6347 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6348 assert!(!parsed.user_invocable);
6349 }
6350 _ => panic!("Expected InlineDirectory"),
6351 }
6352 }
6353
6354 #[tokio::test]
6355 async fn test_contribute_skills_default_empty() {
6356 let mut registry = CapabilityRegistry::new();
6359 registry.register(FilterTestCapability { priority: 0 });
6360
6361 let configs = vec![AgentCapabilityConfig {
6362 capability_ref: CapabilityId::new("filter_test"),
6363 config: serde_json::json!({}),
6364 }];
6365
6366 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6367 assert!(
6368 collected
6369 .mounts
6370 .iter()
6371 .all(|m| !m.path.starts_with("/.agents/skills/"))
6372 );
6373 }
6374
6375 struct LocalizedCapability;
6376
6377 impl Capability for LocalizedCapability {
6378 fn id(&self) -> &str {
6379 "localized"
6380 }
6381 fn name(&self) -> &str {
6382 "Localized"
6383 }
6384 fn description(&self) -> &str {
6385 "English description"
6386 }
6387 fn localizations(&self) -> Vec<CapabilityLocalization> {
6388 vec![
6389 CapabilityLocalization {
6390 locale: "en",
6391 name: None,
6392 description: None,
6393 config_description: Some("Controls things."),
6394 config_overlay: None,
6395 },
6396 CapabilityLocalization {
6397 locale: "uk",
6398 name: Some("Локалізована"),
6399 description: Some("Український опис"),
6400 config_description: Some("Керує налаштуваннями."),
6401 config_overlay: None,
6402 },
6403 ]
6404 }
6405 }
6406
6407 #[test]
6408 fn localized_name_falls_back_exact_language_then_base() {
6409 let cap = LocalizedCapability;
6410 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6412 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6413 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6415 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6417 assert_eq!(cap.localized_name(None), "Localized");
6418 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6419 assert_eq!(cap.localized_description(Some("de")), "English description");
6420 }
6421
6422 #[test]
6423 fn describe_schema_resolves_config_description_per_locale() {
6424 let cap = LocalizedCapability;
6425 assert_eq!(
6426 cap.describe_schema(Some("uk-UA")).as_deref(),
6427 Some("Керує налаштуваннями.")
6428 );
6429 assert_eq!(
6431 cap.describe_schema(Some("pl")).as_deref(),
6432 Some("Controls things.")
6433 );
6434 assert_eq!(
6435 cap.describe_schema(None).as_deref(),
6436 Some("Controls things.")
6437 );
6438 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6440 }
6441}