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