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]
3517 fn builtin_tools_have_narration_or_documented_generic_fallback() {
3518 use crate::tool_narration::{ToolNarrationContext, ToolNarrationPhase};
3519 use crate::tool_types::ToolCall;
3520
3521 const GENERIC_NARRATION_ALLOWLIST: &[(&str, &str)] = &[
3524 ("sample_data", "demo capability with fixture mounts"),
3526 (
3527 "data_knowledge",
3528 "demo knowledge scaffold; fixture data only",
3529 ),
3530 ("fake_aws", "demo/eval fixture tools"),
3531 ("fake_crm", "demo/eval fixture tools"),
3532 ("fake_financial", "demo/eval fixture tools"),
3533 ("fake_warehouse", "demo/eval fixture tools"),
3534 ("test_math", "test fixture capability"),
3535 ("test_weather", "test fixture capability"),
3536 (
3541 "platform_management",
3542 "operator admin surface; mutations narrate via narration_noun, reads use display names",
3543 ),
3544 (
3547 "model_scout",
3548 "operator model-routing tools; display-name presentation is adequate",
3549 ),
3550 (
3551 "openrouter_workspace",
3552 "operator OpenRouter inspection tools; display-name presentation is adequate",
3553 ),
3554 (
3557 "lua",
3558 "arbitrary sandboxed code execution; display-name presentation is adequate",
3559 ),
3560 ];
3561
3562 let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
3565 let ctx = ToolNarrationContext::default();
3566 let mut missing: Vec<String> = Vec::new();
3567
3568 for cap in registry.list() {
3569 let cap_id = cap.id().to_string();
3570 if GENERIC_NARRATION_ALLOWLIST
3571 .iter()
3572 .any(|(id, _)| *id == cap_id)
3573 {
3574 continue;
3575 }
3576
3577 for tool in cap.tools() {
3578 let def = tool.to_definition();
3579 if def.hints().narration_noun.is_some() {
3582 continue;
3583 }
3584
3585 let call = ToolCall {
3586 id: "call_narration_audit".to_string(),
3587 name: tool.name().to_string(),
3588 arguments: serde_json::json!({}),
3589 };
3590 if cap
3593 .narrate(Some(&def), &call, ToolNarrationPhase::Started, None, ctx)
3594 .is_none()
3595 {
3596 missing.push(format!("{cap_id}::{}", tool.name()));
3597 }
3598 }
3599 }
3600
3601 assert!(
3602 missing.is_empty(),
3603 "These built-in tools fall back to raw tool-call presentation. Implement \
3604 `Tool::narrate` (see specs/tool-narration.md), set a `narration_noun` hint, \
3605 or add a documented entry to GENERIC_NARRATION_ALLOWLIST: {missing:?}"
3606 );
3607 }
3608
3609 #[test]
3610 fn test_capability_registry_blueprint_with_capability() {
3611 struct BlueprintProviderCapability;
3612
3613 impl Capability for BlueprintProviderCapability {
3614 fn id(&self) -> &str {
3615 "blueprint_provider"
3616 }
3617 fn name(&self) -> &str {
3618 "Blueprint Provider"
3619 }
3620 fn description(&self) -> &str {
3621 "Capability that provides a blueprint for tests"
3622 }
3623 fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
3624 vec![AgentBlueprint {
3625 id: "test_blueprint",
3626 name: "Test Blueprint",
3627 description: "Blueprint for capability registry tests",
3628 model: BlueprintModel::Inherit,
3629 system_prompt: "Test prompt",
3630 tools: vec![],
3631 max_turns: None,
3632 config_schema: None,
3633 }]
3634 }
3635 }
3636
3637 let mut registry = CapabilityRegistry::new();
3638 registry.register(BlueprintProviderCapability);
3639
3640 let (capability_id, blueprint) = registry
3641 .blueprint_with_capability("test_blueprint")
3642 .expect("blueprint should resolve with capability id");
3643 assert_eq!(capability_id, "blueprint_provider");
3644 assert_eq!(blueprint.id, "test_blueprint");
3645 }
3646
3647 #[test]
3648 fn test_capability_registry_builder() {
3649 let registry = CapabilityRegistry::builder()
3650 .capability(NoopCapability)
3651 .capability(CurrentTimeCapability)
3652 .build();
3653
3654 assert!(registry.has("noop"));
3655 assert!(registry.has("current_time"));
3656 assert_eq!(registry.len(), 2);
3657 }
3658
3659 #[test]
3660 fn test_capability_status() {
3661 let registry = CapabilityRegistry::with_builtins();
3662
3663 let current_time = registry.get("current_time").unwrap();
3664 assert_eq!(current_time.status(), CapabilityStatus::Available);
3665
3666 let research = registry.get("research").unwrap();
3667 assert_eq!(research.status(), CapabilityStatus::ComingSoon);
3668 }
3669
3670 #[test]
3671 fn test_capability_icons_and_categories() {
3672 let registry = CapabilityRegistry::with_builtins();
3673
3674 let noop = registry.get("noop").unwrap();
3675 assert_eq!(noop.icon(), Some("circle-off"));
3676 assert_eq!(noop.category(), Some("Testing"));
3677
3678 let current_time = registry.get("current_time").unwrap();
3679 assert_eq!(current_time.icon(), Some("clock"));
3680 assert_eq!(current_time.category(), Some("Core"));
3681 }
3682
3683 #[test]
3684 fn test_system_prompt_preview_default_delegates_to_addition() {
3685 let registry = CapabilityRegistry::with_builtins();
3686
3687 let test_math = registry.get("test_math").unwrap();
3689 assert_eq!(
3690 test_math.system_prompt_preview().as_deref(),
3691 test_math.system_prompt_addition()
3692 );
3693
3694 let current_time = registry.get("current_time").unwrap();
3696 assert!(current_time.system_prompt_preview().is_none());
3697 assert!(current_time.system_prompt_addition().is_none());
3698 }
3699
3700 #[test]
3701 fn test_system_prompt_preview_dynamic_capability() {
3702 let registry = CapabilityRegistry::with_builtins();
3703 let cap = registry.get("agent_instructions").unwrap();
3704
3705 assert!(cap.system_prompt_addition().is_none());
3707 assert!(cap.system_prompt_preview().is_some());
3708 assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
3709 }
3710
3711 #[tokio::test]
3716 async fn test_apply_capabilities_empty() {
3717 let registry = CapabilityRegistry::with_builtins();
3718 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3719
3720 let applied =
3721 apply_capabilities(base_runtime_agent.clone(), &[], ®istry, &test_ctx()).await;
3722
3723 assert_eq!(
3724 applied.runtime_agent.system_prompt,
3725 base_runtime_agent.system_prompt
3726 );
3727 assert!(applied.tool_registry.is_empty());
3728 assert!(applied.applied_ids.is_empty());
3729 }
3730
3731 #[tokio::test]
3732 async fn test_apply_capabilities_noop() {
3733 let registry = CapabilityRegistry::with_builtins();
3734 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3735
3736 let applied = apply_capabilities(
3737 base_runtime_agent.clone(),
3738 &["noop".to_string()],
3739 ®istry,
3740 &test_ctx(),
3741 )
3742 .await;
3743
3744 assert_eq!(
3746 applied.runtime_agent.system_prompt,
3747 base_runtime_agent.system_prompt
3748 );
3749 assert!(applied.tool_registry.is_empty());
3750 assert_eq!(applied.applied_ids, vec!["noop"]);
3751 }
3752
3753 #[tokio::test]
3754 async fn test_apply_capabilities_current_time() {
3755 let registry = CapabilityRegistry::with_builtins();
3756 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3757
3758 let applied = apply_capabilities(
3759 base_runtime_agent.clone(),
3760 &["current_time".to_string()],
3761 ®istry,
3762 &test_ctx(),
3763 )
3764 .await;
3765
3766 assert!(
3770 applied
3771 .runtime_agent
3772 .system_prompt
3773 .contains(FACTS_DYNAMIC_NOTE),
3774 "current_time should contribute the dynamic-facts note"
3775 );
3776 assert!(
3777 applied
3778 .runtime_agent
3779 .system_prompt
3780 .contains(&base_runtime_agent.system_prompt),
3781 "base prompt is preserved"
3782 );
3783 assert!(applied.tool_registry.has("get_current_time"));
3784 assert_eq!(applied.tool_registry.len(), 1);
3785 assert_eq!(applied.applied_ids, vec!["current_time"]);
3786 }
3787
3788 #[tokio::test]
3789 async fn test_apply_capabilities_skips_coming_soon() {
3790 let registry = CapabilityRegistry::with_builtins();
3791 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3792
3793 let applied = apply_capabilities(
3795 base_runtime_agent.clone(),
3796 &["research".to_string()],
3797 ®istry,
3798 &test_ctx(),
3799 )
3800 .await;
3801
3802 assert_eq!(
3804 applied.runtime_agent.system_prompt,
3805 base_runtime_agent.system_prompt
3806 );
3807 assert!(applied.applied_ids.is_empty()); }
3809
3810 #[tokio::test]
3811 async fn test_apply_capabilities_multiple() {
3812 let registry = CapabilityRegistry::with_builtins();
3813 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3814
3815 let applied = apply_capabilities(
3816 base_runtime_agent.clone(),
3817 &["noop".to_string(), "current_time".to_string()],
3818 ®istry,
3819 &test_ctx(),
3820 )
3821 .await;
3822
3823 assert!(applied.tool_registry.has("get_current_time"));
3824 assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
3825 }
3826
3827 #[tokio::test]
3828 async fn test_apply_capabilities_preserves_order() {
3829 let registry = CapabilityRegistry::with_builtins();
3830 let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
3831
3832 let applied = apply_capabilities(
3834 base_runtime_agent,
3835 &["current_time".to_string(), "noop".to_string()],
3836 ®istry,
3837 &test_ctx(),
3838 )
3839 .await;
3840
3841 assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
3842 }
3843
3844 #[tokio::test]
3845 async fn test_apply_capabilities_test_math() {
3846 let registry = CapabilityRegistry::with_builtins();
3847 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3848
3849 let applied = apply_capabilities(
3850 base_runtime_agent.clone(),
3851 &["test_math".to_string()],
3852 ®istry,
3853 &test_ctx(),
3854 )
3855 .await;
3856
3857 assert!(
3859 !applied
3860 .runtime_agent
3861 .system_prompt
3862 .contains("<capability id=\"test_math\">")
3863 );
3864 assert!(
3866 applied
3867 .runtime_agent
3868 .system_prompt
3869 .contains("You are a helpful assistant.")
3870 );
3871 assert!(applied.tool_registry.has("add"));
3872 assert!(applied.tool_registry.has("subtract"));
3873 assert!(applied.tool_registry.has("multiply"));
3874 assert!(applied.tool_registry.has("divide"));
3875 assert_eq!(applied.tool_registry.len(), 4);
3876 }
3877
3878 #[tokio::test]
3879 async fn test_apply_capabilities_test_weather() {
3880 let registry = CapabilityRegistry::with_builtins();
3881 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3882
3883 let applied = apply_capabilities(
3884 base_runtime_agent.clone(),
3885 &["test_weather".to_string()],
3886 ®istry,
3887 &test_ctx(),
3888 )
3889 .await;
3890
3891 assert!(
3893 !applied
3894 .runtime_agent
3895 .system_prompt
3896 .contains("<capability id=\"test_weather\">")
3897 );
3898 assert!(applied.tool_registry.has("get_weather"));
3899 assert!(applied.tool_registry.has("get_forecast"));
3900 assert_eq!(applied.tool_registry.len(), 2);
3901 }
3902
3903 #[tokio::test]
3904 async fn test_apply_capabilities_test_math_and_test_weather() {
3905 let registry = CapabilityRegistry::with_builtins();
3906 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3907
3908 let applied = apply_capabilities(
3909 base_runtime_agent.clone(),
3910 &["test_math".to_string(), "test_weather".to_string()],
3911 ®istry,
3912 &test_ctx(),
3913 )
3914 .await;
3915
3916 assert_eq!(applied.tool_registry.len(), 6); assert!(applied.tool_registry.has("add"));
3919 assert!(applied.tool_registry.has("get_weather"));
3920 }
3921
3922 #[tokio::test]
3923 async fn test_apply_capabilities_stateless_todo_list() {
3924 let registry = CapabilityRegistry::with_builtins();
3925 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3926
3927 let applied = apply_capabilities(
3928 base_runtime_agent.clone(),
3929 &["stateless_todo_list".to_string()],
3930 ®istry,
3931 &test_ctx(),
3932 )
3933 .await;
3934
3935 assert!(
3937 applied
3938 .runtime_agent
3939 .system_prompt
3940 .contains("Task Management")
3941 );
3942 assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
3943 assert!(applied.tool_registry.has("write_todos"));
3944 assert_eq!(applied.tool_registry.len(), 1);
3945 }
3946
3947 #[tokio::test]
3948 async fn test_apply_capabilities_web_fetch() {
3949 let registry = CapabilityRegistry::with_builtins();
3950 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
3951
3952 let applied = apply_capabilities(
3953 base_runtime_agent.clone(),
3954 &["web_fetch".to_string()],
3955 ®istry,
3956 &test_ctx(),
3957 )
3958 .await;
3959
3960 assert!(
3962 applied
3963 .runtime_agent
3964 .system_prompt
3965 .contains(&base_runtime_agent.system_prompt)
3966 );
3967 assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
3968 assert!(applied.tool_registry.has("web_fetch"));
3969 assert_eq!(applied.tool_registry.len(), 1);
3970 }
3971
3972 #[tokio::test]
3977 async fn test_xml_tags_wrap_capability_prompts() {
3978 let registry = CapabilityRegistry::with_builtins();
3979 let collected =
3980 collect_capabilities(&["stateless_todo_list".to_string()], ®istry, &test_ctx())
3981 .await;
3982
3983 assert_eq!(collected.system_prompt_parts.len(), 1);
3984 let part = &collected.system_prompt_parts[0];
3985 assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
3986 assert!(part.ends_with("</capability>"));
3987 assert!(part.contains("Task Management"));
3988 }
3989
3990 #[tokio::test]
3991 async fn test_xml_tags_multiple_capabilities() {
3992 let registry = CapabilityRegistry::with_builtins();
3993 let collected = collect_capabilities(
3994 &[
3995 "stateless_todo_list".to_string(),
3996 "session_schedule".to_string(),
3997 ],
3998 ®istry,
3999 &test_ctx(),
4000 )
4001 .await;
4002
4003 assert_eq!(collected.system_prompt_parts.len(), 2);
4004 assert!(
4005 collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
4006 );
4007 assert!(
4008 collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
4009 );
4010
4011 let prefix = collected.system_prompt_prefix().unwrap();
4012 assert!(prefix.contains("</capability>\n\n<capability"));
4014 }
4015
4016 #[tokio::test]
4017 async fn test_xml_tags_system_prompt_wrapping() {
4018 let registry = CapabilityRegistry::with_builtins();
4019 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4020
4021 let applied = apply_capabilities(
4022 base,
4023 &["stateless_todo_list".to_string()],
4024 ®istry,
4025 &test_ctx(),
4026 )
4027 .await;
4028
4029 let prompt = &applied.runtime_agent.system_prompt;
4030 assert!(prompt.starts_with("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4031 assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
4033 assert!(prompt.contains("</capability>"));
4034 assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
4036 }
4037
4038 #[tokio::test]
4039 async fn test_no_xml_wrapping_without_capabilities() {
4040 let registry = CapabilityRegistry::with_builtins();
4041 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4042
4043 let applied = apply_capabilities(base, &[], ®istry, &test_ctx()).await;
4044
4045 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4047 assert!(
4048 !applied
4049 .runtime_agent
4050 .system_prompt
4051 .contains("<system-prompt>")
4052 );
4053 }
4054
4055 #[tokio::test]
4056 async fn test_no_xml_wrapping_for_noop_capability() {
4057 let registry = CapabilityRegistry::with_builtins();
4058 let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
4059
4060 let applied = apply_capabilities(base, &["noop".to_string()], ®istry, &test_ctx()).await;
4062
4063 assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
4064 assert!(
4065 !applied
4066 .runtime_agent
4067 .system_prompt
4068 .contains("<system-prompt>")
4069 );
4070 }
4071
4072 #[tokio::test]
4077 async fn test_collect_capabilities_includes_mounts() {
4078 let registry = CapabilityRegistry::with_builtins();
4079
4080 let collected =
4081 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
4082
4083 assert!(!collected.mounts.is_empty());
4084 assert_eq!(collected.mounts.len(), 1);
4085 assert_eq!(collected.mounts[0].path, "/samples");
4086 assert!(collected.mounts[0].is_readonly());
4087 }
4088
4089 #[tokio::test]
4090 async fn test_collect_capabilities_empty_mounts_by_default() {
4091 let registry = CapabilityRegistry::with_builtins();
4092
4093 let collected =
4095 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
4096
4097 assert!(collected.mounts.is_empty());
4098 }
4099
4100 #[tokio::test]
4101 async fn test_dynamic_facts_add_note_without_static_block() {
4102 let registry = CapabilityRegistry::with_builtins();
4106 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4107 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4108 let prompt = collected.system_prompt_parts.join("\n");
4109 assert!(
4110 prompt.contains(FACTS_DYNAMIC_NOTE),
4111 "dynamic-facts note should be in the cached prompt"
4112 );
4113 assert!(
4114 !prompt.contains("<facts>\n"),
4115 "no static <facts> block for a purely-dynamic fact; got: {prompt}"
4116 );
4117 }
4118
4119 #[tokio::test]
4120 async fn test_static_facts_fold_into_prompt() {
4121 struct StaticFactCap;
4122 impl Capability for StaticFactCap {
4123 fn id(&self) -> &str {
4124 "test_static_fact"
4125 }
4126 fn name(&self) -> &str {
4127 "Static Fact"
4128 }
4129 fn description(&self) -> &str {
4130 "test"
4131 }
4132 fn status(&self) -> CapabilityStatus {
4133 CapabilityStatus::Available
4134 }
4135 fn facts(&self, _config: &serde_json::Value, _ctx: &FactsContext) -> Vec<Fact> {
4136 vec![Fact::stat("workspace_root", "/workspace")]
4137 }
4138 }
4139 let mut registry = CapabilityRegistry::new();
4140 registry.register(StaticFactCap);
4141 let configs = vec![AgentCapabilityConfig::new("test_static_fact".to_string())];
4142 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4143 let prompt = collected.system_prompt_parts.join("\n");
4144 assert!(
4145 prompt.contains("<facts>\n- workspace_root: /workspace\n</facts>"),
4146 "static fact should fold into the cached prompt; got: {prompt}"
4147 );
4148 assert!(
4149 !prompt.contains(FACTS_DYNAMIC_NOTE),
4150 "no dynamic note when only static facts exist"
4151 );
4152 }
4153
4154 #[test]
4155 fn test_collect_dynamic_facts_returns_current_time() {
4156 let registry = CapabilityRegistry::with_builtins();
4157 let configs = vec![AgentCapabilityConfig::new("current_time".to_string())];
4158 let facts = collect_dynamic_facts(
4159 &configs,
4160 ®istry,
4161 None,
4162 &FactsContext::new(SessionId::new()),
4163 );
4164 assert_eq!(facts.len(), 1);
4165 assert_eq!(facts[0].key, "current_time");
4166 assert_eq!(facts[0].volatility, Volatility::Dynamic);
4167 }
4168
4169 #[tokio::test]
4170 async fn test_collect_capabilities_combines_mounts() {
4171 let registry = CapabilityRegistry::with_builtins();
4172
4173 let collected = collect_capabilities(
4176 &["sample_data".to_string(), "current_time".to_string()],
4177 ®istry,
4178 &test_ctx(),
4179 )
4180 .await;
4181
4182 assert_eq!(collected.mounts.len(), 1);
4183 assert!(
4185 collected
4186 .applied_ids
4187 .iter()
4188 .any(|id| id == "session_file_system")
4189 );
4190 assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
4191 assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
4192 }
4193
4194 #[test]
4195 fn test_sample_data_capability() {
4196 let registry = CapabilityRegistry::with_builtins();
4197 let cap = registry.get("sample_data").unwrap();
4198
4199 assert_eq!(cap.id(), "sample_data");
4200 assert_eq!(cap.name(), "Sample Data");
4201 assert_eq!(cap.status(), CapabilityStatus::Available);
4202
4203 assert!(cap.system_prompt_addition().is_some());
4205 assert!(cap.tools().is_empty());
4206
4207 assert!(!cap.mounts().is_empty());
4209 }
4210
4211 #[test]
4216 fn test_resolve_dependencies_empty() {
4217 let registry = CapabilityRegistry::with_builtins();
4218
4219 let resolved = resolve_dependencies(&[], ®istry).unwrap();
4220
4221 assert!(resolved.resolved_ids.is_empty());
4222 assert!(resolved.added_as_dependencies.is_empty());
4223 assert!(resolved.user_selected.is_empty());
4224 }
4225
4226 #[test]
4227 fn test_resolve_dependencies_no_deps() {
4228 let registry = CapabilityRegistry::with_builtins();
4229
4230 let resolved = resolve_dependencies(&["current_time".to_string()], ®istry).unwrap();
4232
4233 assert_eq!(resolved.resolved_ids, vec!["current_time"]);
4234 assert!(resolved.added_as_dependencies.is_empty());
4235 }
4236
4237 #[test]
4238 fn test_resolve_dependencies_with_deps() {
4239 let registry = CapabilityRegistry::with_builtins();
4240
4241 let resolved = resolve_dependencies(&["sample_data".to_string()], ®istry).unwrap();
4243
4244 assert_eq!(resolved.resolved_ids.len(), 2);
4246 let fs_pos = resolved
4247 .resolved_ids
4248 .iter()
4249 .position(|id| id == "session_file_system")
4250 .unwrap();
4251 let sd_pos = resolved
4252 .resolved_ids
4253 .iter()
4254 .position(|id| id == "sample_data")
4255 .unwrap();
4256 assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
4257
4258 assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
4260 }
4261
4262 #[test]
4263 fn test_resolve_dependencies_already_selected() {
4264 let registry = CapabilityRegistry::with_builtins();
4265
4266 let resolved = resolve_dependencies(
4268 &["session_file_system".to_string(), "sample_data".to_string()],
4269 ®istry,
4270 )
4271 .unwrap();
4272
4273 assert_eq!(resolved.resolved_ids.len(), 2);
4274 assert!(resolved.added_as_dependencies.is_empty());
4276 }
4277
4278 #[test]
4279 fn test_resolve_dependencies_preserves_order() {
4280 let registry = CapabilityRegistry::with_builtins();
4281
4282 let resolved =
4284 resolve_dependencies(&["current_time".to_string(), "noop".to_string()], ®istry)
4285 .unwrap();
4286
4287 assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
4288 }
4289
4290 #[test]
4291 fn test_resolve_dependencies_unknown_capability() {
4292 let registry = CapabilityRegistry::with_builtins();
4293
4294 let resolved =
4296 resolve_dependencies(&["unknown_capability".to_string()], ®istry).unwrap();
4297
4298 assert!(resolved.resolved_ids.is_empty());
4299 }
4300
4301 #[test]
4302 fn test_get_dependencies() {
4303 let registry = CapabilityRegistry::with_builtins();
4304
4305 let deps = get_dependencies("sample_data", ®istry);
4307 assert_eq!(deps, vec!["session_file_system"]);
4308
4309 let deps = get_dependencies("current_time", ®istry);
4311 assert!(deps.is_empty());
4312
4313 let deps = get_dependencies("unknown", ®istry);
4315 assert!(deps.is_empty());
4316 }
4317
4318 #[test]
4319 fn test_sample_data_has_dependency() {
4320 let registry = CapabilityRegistry::with_builtins();
4321 let cap = registry.get("sample_data").unwrap();
4322
4323 let deps = cap.dependencies();
4324 assert_eq!(deps.len(), 1);
4325 assert_eq!(deps[0], "session_file_system");
4326 }
4327
4328 #[test]
4329 fn test_noop_has_no_dependencies() {
4330 let registry = CapabilityRegistry::with_builtins();
4331 let cap = registry.get("noop").unwrap();
4332
4333 assert!(cap.dependencies().is_empty());
4334 }
4335
4336 #[test]
4340 fn test_circular_dependency_error() {
4341 struct CapA;
4343 struct CapB;
4344
4345 impl Capability for CapA {
4346 fn id(&self) -> &str {
4347 "test_cap_a"
4348 }
4349 fn name(&self) -> &str {
4350 "Test A"
4351 }
4352 fn description(&self) -> &str {
4353 "Test capability A"
4354 }
4355 fn dependencies(&self) -> Vec<&'static str> {
4356 vec!["test_cap_b"]
4357 }
4358 }
4359
4360 impl Capability for CapB {
4361 fn id(&self) -> &str {
4362 "test_cap_b"
4363 }
4364 fn name(&self) -> &str {
4365 "Test B"
4366 }
4367 fn description(&self) -> &str {
4368 "Test capability B"
4369 }
4370 fn dependencies(&self) -> Vec<&'static str> {
4371 vec!["test_cap_a"]
4372 }
4373 }
4374
4375 let mut registry = CapabilityRegistry::new();
4376 registry.register(CapA);
4377 registry.register(CapB);
4378
4379 let result = resolve_dependencies(&["test_cap_a".to_string()], ®istry);
4380
4381 assert!(result.is_err());
4382 match result.unwrap_err() {
4383 DependencyError::CircularDependency { capability_id, .. } => {
4384 assert_eq!(capability_id, "test_cap_a");
4385 }
4386 _ => panic!("Expected CircularDependency error"),
4387 }
4388 }
4389
4390 use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
4395
4396 struct FilterTestCapability {
4398 priority: i32,
4399 }
4400
4401 impl Capability for FilterTestCapability {
4402 fn id(&self) -> &str {
4403 "filter_test"
4404 }
4405 fn name(&self) -> &str {
4406 "Filter Test"
4407 }
4408 fn description(&self) -> &str {
4409 "Test capability with message filter"
4410 }
4411 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4412 Some(Arc::new(FilterTestProvider {
4413 priority: self.priority,
4414 }))
4415 }
4416 }
4417
4418 struct FilterTestProvider {
4419 priority: i32,
4420 }
4421
4422 impl MessageFilterProvider for FilterTestProvider {
4423 fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
4424 if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
4426 query
4427 .filters
4428 .push(MessageFilter::Search(search.to_string()));
4429 }
4430 }
4431
4432 fn priority(&self) -> i32 {
4433 self.priority
4434 }
4435 }
4436
4437 #[tokio::test]
4438 async fn test_collect_capabilities_with_configs_no_filter_providers() {
4439 let registry = CapabilityRegistry::with_builtins();
4440 let configs = vec![AgentCapabilityConfig {
4441 capability_ref: CapabilityId::new("current_time"),
4442 config: serde_json::json!({}),
4443 }];
4444
4445 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4446
4447 assert!(collected.message_filter_providers.is_empty());
4448 assert!(!collected.has_message_filters());
4449 }
4450
4451 #[tokio::test]
4452 async fn test_collect_capabilities_with_configs_with_filter_provider() {
4453 let mut registry = CapabilityRegistry::new();
4454 registry.register(FilterTestCapability { priority: 0 });
4455
4456 let configs = vec![AgentCapabilityConfig {
4457 capability_ref: CapabilityId::new("filter_test"),
4458 config: serde_json::json!({ "search": "hello" }),
4459 }];
4460
4461 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4462
4463 assert_eq!(collected.message_filter_providers.len(), 1);
4464 assert!(collected.has_message_filters());
4465 }
4466
4467 #[tokio::test]
4468 async fn test_collect_capabilities_with_configs_filter_priority_order() {
4469 struct HighPriorityCapability;
4471 struct LowPriorityCapability;
4472
4473 impl Capability for HighPriorityCapability {
4474 fn id(&self) -> &str {
4475 "high_priority"
4476 }
4477 fn name(&self) -> &str {
4478 "High Priority"
4479 }
4480 fn description(&self) -> &str {
4481 "Test"
4482 }
4483 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4484 Some(Arc::new(FilterTestProvider { priority: 10 }))
4485 }
4486 }
4487
4488 impl Capability for LowPriorityCapability {
4489 fn id(&self) -> &str {
4490 "low_priority"
4491 }
4492 fn name(&self) -> &str {
4493 "Low Priority"
4494 }
4495 fn description(&self) -> &str {
4496 "Test"
4497 }
4498 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4499 Some(Arc::new(FilterTestProvider { priority: -5 }))
4500 }
4501 }
4502
4503 let mut registry = CapabilityRegistry::new();
4504 registry.register(HighPriorityCapability);
4505 registry.register(LowPriorityCapability);
4506
4507 let configs = vec![
4509 AgentCapabilityConfig {
4510 capability_ref: CapabilityId::new("high_priority"),
4511 config: serde_json::json!({}),
4512 },
4513 AgentCapabilityConfig {
4514 capability_ref: CapabilityId::new("low_priority"),
4515 config: serde_json::json!({}),
4516 },
4517 ];
4518
4519 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4520
4521 assert_eq!(collected.message_filter_providers.len(), 2);
4523 assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
4524 assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
4525 }
4526
4527 #[tokio::test]
4528 async fn test_collected_capabilities_apply_message_filters() {
4529 let mut registry = CapabilityRegistry::new();
4530 registry.register(FilterTestCapability { priority: 0 });
4531
4532 let configs = vec![AgentCapabilityConfig {
4533 capability_ref: CapabilityId::new("filter_test"),
4534 config: serde_json::json!({ "search": "test_query" }),
4535 }];
4536
4537 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4538
4539 let session_id: SessionId = Uuid::now_v7().into();
4541 let mut query = MessageQuery::new(session_id);
4542
4543 collected.apply_message_filters(&mut query);
4544
4545 assert_eq!(query.filters.len(), 1);
4547 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4548 }
4549
4550 #[tokio::test]
4551 async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
4552 struct SearchCapability {
4553 id: &'static str,
4554 search_term: &'static str,
4555 priority: i32,
4556 }
4557
4558 struct SearchProvider {
4559 search_term: &'static str,
4560 priority: i32,
4561 }
4562
4563 impl MessageFilterProvider for SearchProvider {
4564 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4565 query
4566 .filters
4567 .push(MessageFilter::Search(self.search_term.to_string()));
4568 }
4569
4570 fn priority(&self) -> i32 {
4571 self.priority
4572 }
4573 }
4574
4575 impl Capability for SearchCapability {
4576 fn id(&self) -> &str {
4577 self.id
4578 }
4579 fn name(&self) -> &str {
4580 "Search"
4581 }
4582 fn description(&self) -> &str {
4583 "Test"
4584 }
4585 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4586 Some(Arc::new(SearchProvider {
4587 search_term: self.search_term,
4588 priority: self.priority,
4589 }))
4590 }
4591 }
4592
4593 let mut registry = CapabilityRegistry::new();
4594 registry.register(SearchCapability {
4595 id: "cap_a",
4596 search_term: "alpha",
4597 priority: 5,
4598 });
4599 registry.register(SearchCapability {
4600 id: "cap_b",
4601 search_term: "beta",
4602 priority: 1,
4603 });
4604 registry.register(SearchCapability {
4605 id: "cap_c",
4606 search_term: "gamma",
4607 priority: 10,
4608 });
4609
4610 let configs = vec![
4611 AgentCapabilityConfig {
4612 capability_ref: CapabilityId::new("cap_a"),
4613 config: serde_json::json!({}),
4614 },
4615 AgentCapabilityConfig {
4616 capability_ref: CapabilityId::new("cap_b"),
4617 config: serde_json::json!({}),
4618 },
4619 AgentCapabilityConfig {
4620 capability_ref: CapabilityId::new("cap_c"),
4621 config: serde_json::json!({}),
4622 },
4623 ];
4624
4625 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4626
4627 let session_id: SessionId = Uuid::now_v7().into();
4628 let mut query = MessageQuery::new(session_id);
4629
4630 collected.apply_message_filters(&mut query);
4631
4632 assert_eq!(query.filters.len(), 3);
4634 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4635 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4636 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4637 }
4638
4639 #[test]
4640 fn test_capability_without_message_filter_returns_none() {
4641 let registry = CapabilityRegistry::with_builtins();
4642
4643 let noop = registry.get("noop").unwrap();
4644 assert!(noop.message_filter_provider().is_none());
4645
4646 let current_time = registry.get("current_time").unwrap();
4647 assert!(current_time.message_filter_provider().is_none());
4648 }
4649
4650 #[tokio::test]
4651 async fn test_collect_capabilities_preserves_config_for_filter_provider() {
4652 let mut registry = CapabilityRegistry::new();
4653 registry.register(FilterTestCapability { priority: 0 });
4654
4655 let test_config = serde_json::json!({
4656 "search": "custom_search",
4657 "extra_field": 42
4658 });
4659
4660 let configs = vec![AgentCapabilityConfig {
4661 capability_ref: CapabilityId::new("filter_test"),
4662 config: test_config.clone(),
4663 }];
4664
4665 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
4666
4667 assert_eq!(collected.message_filter_providers.len(), 1);
4669 let (_, stored_config) = &collected.message_filter_providers[0];
4670 assert_eq!(*stored_config, test_config);
4671 }
4672
4673 #[test]
4678 fn test_collect_message_filters_only_collects_filters() {
4679 let mut registry = CapabilityRegistry::new();
4680 registry.register(FilterTestCapability { priority: 0 });
4681
4682 let configs = vec![AgentCapabilityConfig {
4683 capability_ref: CapabilityId::new("filter_test"),
4684 config: serde_json::json!({ "search": "test_query" }),
4685 }];
4686
4687 let collected = collect_message_filters_only(&configs, ®istry);
4688
4689 let session_id: SessionId = Uuid::now_v7().into();
4690 let mut query = MessageQuery::new(session_id);
4691 collected.apply_message_filters(&mut query);
4692
4693 assert_eq!(query.filters.len(), 1);
4694 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
4695 }
4696
4697 #[test]
4698 fn test_message_filter_config_injects_compaction_active_for_infinity_context() {
4699 let base = serde_json::json!({ "context_budget_tokens": 1000 });
4700
4701 let with = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, true);
4703 assert_eq!(with["compaction_active"], serde_json::json!(true));
4704 assert_eq!(with["context_budget_tokens"], serde_json::json!(1000));
4705
4706 let without = message_filter_config_for(INFINITY_CONTEXT_CAPABILITY_ID, &base, false);
4707 assert!(without.get("compaction_active").is_none());
4708
4709 let other = message_filter_config_for("other", &base, true);
4711 assert!(other.get("compaction_active").is_none());
4712
4713 let null_base = message_filter_config_for(
4715 INFINITY_CONTEXT_CAPABILITY_ID,
4716 &serde_json::Value::Null,
4717 true,
4718 );
4719 assert_eq!(null_base["compaction_active"], serde_json::json!(true));
4720 }
4721
4722 #[test]
4723 fn test_infinity_context_defers_to_compaction_end_to_end() {
4724 use crate::message::Message;
4725
4726 let mut registry = CapabilityRegistry::new();
4727 registry.register(InfinityContextCapability);
4728 registry.register(CompactionCapability);
4729
4730 let tight = serde_json::json!({
4731 "context_budget_tokens": 1,
4732 "min_recent_messages": 1
4733 });
4734
4735 let solo = vec![AgentCapabilityConfig {
4737 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4738 config: tight.clone(),
4739 }];
4740 let mut messages = vec![
4741 Message::user("task"),
4742 Message::assistant("old ".repeat(400)),
4743 Message::user("recent"),
4744 ];
4745 collect_message_filters_only(&solo, ®istry).apply_post_load_filters(&mut messages);
4746 assert!(
4747 messages
4748 .iter()
4749 .any(|m| m.text().is_some_and(|t| t.contains("NOT visible"))),
4750 "infinity context alone should trim and notice"
4751 );
4752
4753 let both = vec![
4755 AgentCapabilityConfig {
4756 capability_ref: CapabilityId::new(INFINITY_CONTEXT_CAPABILITY_ID),
4757 config: tight,
4758 },
4759 AgentCapabilityConfig {
4760 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4761 config: serde_json::json!({}),
4762 },
4763 ];
4764 let mut messages = vec![
4765 Message::user("task"),
4766 Message::assistant("old ".repeat(400)),
4767 Message::user("recent"),
4768 ];
4769 collect_message_filters_only(&both, ®istry).apply_post_load_filters(&mut messages);
4770 assert_eq!(messages.len(), 3, "compaction owns reduction; no eviction");
4771 assert!(
4772 messages
4773 .iter()
4774 .all(|m| !m.text().is_some_and(|t| t.contains("NOT visible"))),
4775 "no hidden-history notice when compaction is the active reducer"
4776 );
4777 }
4778
4779 #[test]
4780 fn test_compaction_is_enabled_detects_compaction() {
4781 let mut registry = CapabilityRegistry::new();
4782 registry.register(CompactionCapability);
4783
4784 let with_compaction = vec![AgentCapabilityConfig {
4785 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
4786 config: serde_json::json!({}),
4787 }];
4788 assert!(compaction_is_enabled(&with_compaction, ®istry));
4789
4790 let without = vec![AgentCapabilityConfig {
4791 capability_ref: CapabilityId::new("current_time"),
4792 config: serde_json::json!({}),
4793 }];
4794 assert!(!compaction_is_enabled(&without, ®istry));
4795 }
4796
4797 #[test]
4798 fn test_collect_message_filters_only_skips_unknown_capabilities() {
4799 let registry = CapabilityRegistry::new();
4800
4801 let configs = vec![AgentCapabilityConfig {
4802 capability_ref: CapabilityId::new("nonexistent"),
4803 config: serde_json::json!({}),
4804 }];
4805
4806 let collected = collect_message_filters_only(&configs, ®istry);
4807 assert!(collected.message_filter_providers.is_empty());
4808 }
4809
4810 #[test]
4811 fn test_collect_message_filters_only_preserves_priority_order() {
4812 struct PriorityFilterCap {
4813 id: &'static str,
4814 search_term: &'static str,
4815 priority: i32,
4816 }
4817
4818 struct PriorityFilterProvider {
4819 search_term: &'static str,
4820 priority: i32,
4821 }
4822
4823 impl Capability for PriorityFilterCap {
4824 fn id(&self) -> &str {
4825 self.id
4826 }
4827 fn name(&self) -> &str {
4828 self.id
4829 }
4830 fn description(&self) -> &str {
4831 "priority test"
4832 }
4833 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4834 Some(Arc::new(PriorityFilterProvider {
4835 search_term: self.search_term,
4836 priority: self.priority,
4837 }))
4838 }
4839 }
4840
4841 impl MessageFilterProvider for PriorityFilterProvider {
4842 fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
4843 query
4844 .filters
4845 .push(MessageFilter::Search(self.search_term.to_string()));
4846 }
4847 fn priority(&self) -> i32 {
4848 self.priority
4849 }
4850 }
4851
4852 let mut registry = CapabilityRegistry::new();
4853 registry.register(PriorityFilterCap {
4854 id: "gamma",
4855 search_term: "gamma",
4856 priority: 10,
4857 });
4858 registry.register(PriorityFilterCap {
4859 id: "alpha",
4860 search_term: "alpha",
4861 priority: 5,
4862 });
4863 registry.register(PriorityFilterCap {
4864 id: "beta",
4865 search_term: "beta",
4866 priority: 1,
4867 });
4868
4869 let configs = vec![
4870 AgentCapabilityConfig {
4871 capability_ref: CapabilityId::new("gamma"),
4872 config: serde_json::json!({}),
4873 },
4874 AgentCapabilityConfig {
4875 capability_ref: CapabilityId::new("alpha"),
4876 config: serde_json::json!({}),
4877 },
4878 AgentCapabilityConfig {
4879 capability_ref: CapabilityId::new("beta"),
4880 config: serde_json::json!({}),
4881 },
4882 ];
4883
4884 let collected = collect_message_filters_only(&configs, ®istry);
4885
4886 let session_id: SessionId = Uuid::now_v7().into();
4887 let mut query = MessageQuery::new(session_id);
4888 collected.apply_message_filters(&mut query);
4889
4890 assert_eq!(query.filters.len(), 3);
4892 assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
4893 assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
4894 assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
4895 }
4896
4897 #[test]
4898 fn test_collect_message_filters_only_post_load_invoked() {
4899 use crate::message::Message;
4900
4901 struct PostLoadCap;
4902 struct PostLoadProvider;
4903
4904 impl Capability for PostLoadCap {
4905 fn id(&self) -> &str {
4906 "post_load_test"
4907 }
4908 fn name(&self) -> &str {
4909 "PostLoad Test"
4910 }
4911 fn description(&self) -> &str {
4912 "test"
4913 }
4914 fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
4915 Some(Arc::new(PostLoadProvider))
4916 }
4917 }
4918
4919 impl MessageFilterProvider for PostLoadProvider {
4920 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
4921 fn priority(&self) -> i32 {
4922 0
4923 }
4924 fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
4925 messages.reverse();
4927 }
4928 }
4929
4930 let mut registry = CapabilityRegistry::new();
4931 registry.register(PostLoadCap);
4932
4933 let configs = vec![AgentCapabilityConfig {
4934 capability_ref: CapabilityId::new("post_load_test"),
4935 config: serde_json::json!({}),
4936 }];
4937
4938 let collected = collect_message_filters_only(&configs, ®istry);
4939
4940 let mut messages = vec![Message::user("first"), Message::user("second")];
4941 collected.apply_post_load_filters(&mut messages);
4942
4943 assert_eq!(messages[0].text(), Some("second"));
4945 assert_eq!(messages[1].text(), Some("first"));
4946 }
4947
4948 #[test]
4949 fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
4950 use crate::tool_types::ToolCall;
4951
4952 fn tool_heavy_messages() -> Vec<Message> {
4953 let mut messages = vec![Message::user("inspect files repeatedly")];
4954 for index in 0..9 {
4955 let call_id = format!("call_{index}");
4956 messages.push(Message::assistant_with_tools(
4957 "",
4958 vec![ToolCall {
4959 id: call_id.clone(),
4960 name: "read_file".to_string(),
4961 arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
4962 }],
4963 ));
4964 messages.push(Message::tool_result(
4965 call_id,
4966 Some(serde_json::json!({
4967 "path": "/workspace/src/lib.rs",
4968 "content": format!("{}{}", "large file line\n".repeat(1000), index),
4969 "total_lines": 1000,
4970 "lines_shown": {"start": 1, "end": 1000},
4971 "truncated": false
4972 })),
4973 None,
4974 ));
4975 }
4976 messages
4977 }
4978
4979 fn first_tool_result_is_masked(messages: &[Message]) -> bool {
4980 messages[2]
4981 .tool_result_content()
4982 .and_then(|result| result.result.as_ref())
4983 .and_then(|result| result.get("masked"))
4984 .and_then(|masked| masked.as_bool())
4985 .unwrap_or(false)
4986 }
4987
4988 let mut registry = CapabilityRegistry::new();
4989 registry.register(CompactionCapability);
4990 let context = ModelViewContext {
4991 session_id: SessionId::new(),
4992 prior_usage: None,
4993 };
4994
4995 let no_compaction = collect_model_view_providers(&[], ®istry, None);
4996 let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
4997 assert!(!first_tool_result_is_masked(&unmasked));
4998
4999 let compaction = collect_model_view_providers(
5000 &[AgentCapabilityConfig {
5001 capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
5002 config: serde_json::json!({}),
5003 }],
5004 ®istry,
5005 None,
5006 );
5007 let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
5008 assert!(first_tool_result_is_masked(&masked));
5009 let last_tool = masked.last().unwrap().tool_result_content().unwrap();
5010 assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
5011 }
5012
5013 struct DelegatingFilterCap {
5016 id: &'static str,
5017 inner: std::sync::Arc<InnerFilterCap>,
5018 }
5019 struct InnerFilterCap;
5020
5021 impl Capability for InnerFilterCap {
5022 fn id(&self) -> &str {
5023 "inner_filter"
5024 }
5025 fn name(&self) -> &str {
5026 "Inner Filter"
5027 }
5028 fn description(&self) -> &str {
5029 "inner"
5030 }
5031 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5032 Some(std::sync::Arc::new(SentinelFilter))
5033 }
5034 }
5035 struct SentinelFilter;
5036 impl MessageFilterProvider for SentinelFilter {
5037 fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
5038 }
5039 impl Capability for DelegatingFilterCap {
5040 fn id(&self) -> &str {
5041 self.id
5042 }
5043 fn name(&self) -> &str {
5044 "Delegating Filter"
5045 }
5046 fn description(&self) -> &str {
5047 "delegating"
5048 }
5049 fn message_filter_provider(&self) -> Option<std::sync::Arc<dyn MessageFilterProvider>> {
5050 None }
5052 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5053 Some(&*self.inner)
5054 }
5055 }
5056
5057 #[test]
5058 fn test_collect_message_filters_only_honors_resolve_for_model_delegation() {
5059 let inner = std::sync::Arc::new(InnerFilterCap);
5060 let outer = DelegatingFilterCap {
5061 id: "delegating_filter",
5062 inner: inner.clone(),
5063 };
5064
5065 let mut registry = CapabilityRegistry::new();
5066 registry.register(outer);
5067
5068 let configs = vec![AgentCapabilityConfig {
5069 capability_ref: CapabilityId::new("delegating_filter"),
5070 config: serde_json::json!({}),
5071 }];
5072
5073 let collected = collect_message_filters_only(&configs, ®istry);
5076 assert_eq!(
5077 collected.message_filter_providers.len(),
5078 1,
5079 "provider from resolved inner capability must be collected"
5080 );
5081 }
5082
5083 struct DelegatingMvpCap {
5084 id: &'static str,
5085 inner: std::sync::Arc<InnerMvpCap>,
5086 }
5087 struct InnerMvpCap;
5088
5089 impl Capability for InnerMvpCap {
5090 fn id(&self) -> &str {
5091 "inner_mvp"
5092 }
5093 fn name(&self) -> &str {
5094 "Inner MVP"
5095 }
5096 fn description(&self) -> &str {
5097 "inner"
5098 }
5099 fn model_view_provider(
5100 &self,
5101 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5102 struct NoopMvp;
5104 impl crate::capabilities::ModelViewProvider for NoopMvp {
5105 fn apply_model_view(
5106 &self,
5107 messages: Vec<Message>,
5108 _config: &serde_json::Value,
5109 _context: &ModelViewContext<'_>,
5110 ) -> Vec<Message> {
5111 messages
5112 }
5113 }
5114 Some(std::sync::Arc::new(NoopMvp))
5115 }
5116 }
5117 impl Capability for DelegatingMvpCap {
5118 fn id(&self) -> &str {
5119 self.id
5120 }
5121 fn name(&self) -> &str {
5122 "Delegating MVP"
5123 }
5124 fn description(&self) -> &str {
5125 "delegating"
5126 }
5127 fn model_view_provider(
5128 &self,
5129 ) -> Option<std::sync::Arc<dyn crate::capabilities::ModelViewProvider>> {
5130 None }
5132 fn resolve_for_model(&self, _model: Option<&str>) -> Option<&dyn Capability> {
5133 Some(&*self.inner)
5134 }
5135 }
5136
5137 #[test]
5138 fn test_collect_model_view_providers_honors_resolve_for_model_delegation() {
5139 let inner = std::sync::Arc::new(InnerMvpCap);
5140 let outer = DelegatingMvpCap {
5141 id: "delegating_mvp",
5142 inner: inner.clone(),
5143 };
5144
5145 let mut registry = CapabilityRegistry::new();
5146 registry.register(outer);
5147
5148 let configs = vec![AgentCapabilityConfig {
5149 capability_ref: CapabilityId::new("delegating_mvp"),
5150 config: serde_json::json!({}),
5151 }];
5152
5153 let collected = collect_model_view_providers(&configs, ®istry, None);
5156 assert_eq!(
5157 collected.model_view_providers.len(),
5158 1,
5159 "provider from resolved inner capability must be collected"
5160 );
5161 }
5162
5163 #[tokio::test]
5173 async fn test_bashkit_shell_capability_produces_bash_tool() {
5174 let registry = CapabilityRegistry::with_builtins();
5175 let collected =
5176 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5177
5178 let tool_names: Vec<&str> = collected
5179 .tool_definitions
5180 .iter()
5181 .map(|t| t.name())
5182 .collect();
5183 assert!(
5184 tool_names.contains(&"bash"),
5185 "bashkit_shell capability must produce 'bash' tool, got: {:?}",
5186 tool_names
5187 );
5188 assert!(
5189 !collected.tools.is_empty(),
5190 "bashkit_shell must provide tool implementations"
5191 );
5192 }
5193
5194 #[tokio::test]
5195 async fn test_generic_harness_capability_set_produces_bash_tool() {
5196 let generic_harness_caps = vec![
5199 "session_file_system".to_string(),
5200 "bashkit_shell".to_string(),
5201 "web_fetch".to_string(),
5202 "session_storage".to_string(),
5203 "session".to_string(),
5204 "agent_instructions".to_string(),
5205 "skills".to_string(),
5206 "infinity_context".to_string(),
5207 "auto_tool_search".to_string(),
5208 ];
5209
5210 let registry = CapabilityRegistry::with_builtins();
5211 let collected = collect_capabilities(&generic_harness_caps, ®istry, &test_ctx()).await;
5212
5213 let tool_names: Vec<&str> = collected
5214 .tool_definitions
5215 .iter()
5216 .map(|t| t.name())
5217 .collect();
5218 assert!(
5219 tool_names.contains(&"bash"),
5220 "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
5221 tool_names
5222 );
5223 }
5224
5225 #[tokio::test]
5226 async fn test_collect_capabilities_tool_count_matches_definitions() {
5227 let registry = CapabilityRegistry::with_builtins();
5230 let collected =
5231 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5232
5233 assert_eq!(
5234 collected.tools.len(),
5235 collected.tool_definitions.len(),
5236 "tool implementations ({}) must match tool definitions ({})",
5237 collected.tools.len(),
5238 collected.tool_definitions.len(),
5239 );
5240 }
5241
5242 #[tokio::test]
5246 async fn test_collect_capabilities_resolves_dependencies() {
5247 let registry = CapabilityRegistry::with_builtins();
5250 let collected =
5251 collect_capabilities(&["sample_data".to_string()], ®istry, &test_ctx()).await;
5252
5253 assert!(
5255 collected
5256 .applied_ids
5257 .iter()
5258 .any(|id| id == "session_file_system"),
5259 "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
5260 collected.applied_ids
5261 );
5262
5263 let tool_names: Vec<&str> = collected
5264 .tool_definitions
5265 .iter()
5266 .map(|t| t.name())
5267 .collect();
5268
5269 assert!(
5271 tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
5272 "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
5273 tool_names
5274 );
5275
5276 assert_eq!(
5278 collected.tools.len(),
5279 collected.tool_definitions.len(),
5280 "dependency-added tools must have implementations, not just definitions"
5281 );
5282 }
5283
5284 #[test]
5285 fn test_defaults_do_not_include_bash() {
5286 let registry = crate::ToolRegistry::with_defaults();
5289 assert!(
5290 !registry.has("bash"),
5291 "with_defaults() must not include 'bash' — it comes from bashkit_shell capability"
5292 );
5293 }
5294
5295 #[tokio::test]
5302 async fn test_background_execution_auto_activates_with_bashkit_shell() {
5303 let registry = CapabilityRegistry::with_builtins();
5304 let collected =
5305 collect_capabilities(&["bashkit_shell".to_string()], ®istry, &test_ctx()).await;
5306
5307 let tool_names: Vec<&str> = collected
5308 .tool_definitions
5309 .iter()
5310 .map(|t| t.name())
5311 .collect();
5312 assert!(
5313 tool_names.contains(&"spawn_background"),
5314 "spawn_background must be auto-activated when bashkit_shell (a \
5315 background-capable tool) is in the agent's capability set; got: {:?}",
5316 tool_names
5317 );
5318 assert!(
5319 collected
5320 .applied_ids
5321 .iter()
5322 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5323 "background_execution must be in applied_ids when auto-activated; \
5324 got: {:?}",
5325 collected.applied_ids
5326 );
5327
5328 assert!(
5330 collected
5331 .tools
5332 .iter()
5333 .any(|t| t.name() == "spawn_background"),
5334 "spawn_background tool implementation must be present alongside the \
5335 definition (lockstep contract)"
5336 );
5337 }
5338
5339 #[tokio::test]
5342 async fn test_background_execution_does_not_auto_activate_without_hint() {
5343 let registry = CapabilityRegistry::with_builtins();
5344 let collected =
5346 collect_capabilities(&["current_time".to_string()], ®istry, &test_ctx()).await;
5347
5348 let tool_names: Vec<&str> = collected
5349 .tool_definitions
5350 .iter()
5351 .map(|t| t.name())
5352 .collect();
5353 assert!(
5354 !tool_names.contains(&"spawn_background"),
5355 "spawn_background must NOT be activated without a background-capable \
5356 tool; got: {:?}",
5357 tool_names
5358 );
5359 assert!(
5360 !collected
5361 .applied_ids
5362 .iter()
5363 .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
5364 "background_execution must not appear in applied_ids when no \
5365 background-capable tool is present; got: {:?}",
5366 collected.applied_ids
5367 );
5368 }
5369
5370 #[tokio::test]
5371 async fn test_subagents_collect_unified_spawn_agent_adapter() {
5372 let registry = CapabilityRegistry::with_builtins();
5373 let collected = collect_capabilities(
5374 &[SUBAGENTS_CAPABILITY_ID.to_string()],
5375 ®istry,
5376 &test_ctx(),
5377 )
5378 .await;
5379
5380 assert!(
5381 collected
5382 .tools
5383 .iter()
5384 .any(|tool| tool.name() == "spawn_agent"),
5385 "subagent-only sessions should get the unified spawn_agent adapter"
5386 );
5387 let spawn_agent = collected
5388 .tool_definitions
5389 .iter()
5390 .find(|tool| tool.name() == "spawn_agent")
5391 .expect("spawn_agent definition");
5392 assert_eq!(
5393 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5394 serde_json::json!(["subagent"])
5395 );
5396 assert_eq!(
5397 spawn_agent.concurrency_class(),
5398 Some(SPAWN_AGENT_CONCURRENCY_CLASS),
5399 "unified spawn_agent must serialize same-batch spawns before cap checks"
5400 );
5401 }
5402
5403 #[tokio::test]
5404 async fn test_agent_handoff_collects_unified_spawn_agent_adapter() {
5405 let mut registry = CapabilityRegistry::new();
5406 registry.register(AgentHandoffCapability);
5407 let agent_id = crate::typed_id::AgentId::new();
5408 let harness_id = crate::typed_id::HarnessId::new();
5409 let configs = vec![AgentCapabilityConfig {
5410 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5411 config: serde_json::json!({
5412 "targets": [{
5413 "id": "aws_operator",
5414 "name": "AWS Operator",
5415 "agent_id": agent_id,
5416 "harness_id": harness_id
5417 }]
5418 }),
5419 }];
5420 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5421
5422 assert!(
5423 collected
5424 .tools
5425 .iter()
5426 .any(|tool| tool.name() == "spawn_agent"),
5427 "agent_handoff-only sessions should get the unified spawn_agent adapter"
5428 );
5429 let spawn_agent = collected
5430 .tool_definitions
5431 .iter()
5432 .find(|tool| tool.name() == "spawn_agent")
5433 .expect("spawn_agent definition");
5434 assert_eq!(
5435 spawn_agent.parameters()["properties"]["target"]["properties"]["type"]["enum"],
5436 serde_json::json!(["agent"])
5437 );
5438 }
5439
5440 #[tokio::test]
5441 async fn test_spawn_agent_dispatcher_combines_known_target_providers() {
5442 let mut registry = CapabilityRegistry::new();
5443 registry.register(SubagentCapability);
5444 registry.register(AgentHandoffCapability);
5445
5446 let agent_id = crate::typed_id::AgentId::new();
5447 let harness_id = crate::typed_id::HarnessId::new();
5448 let configs = vec![
5449 AgentCapabilityConfig {
5450 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5451 config: serde_json::json!({}),
5452 },
5453 AgentCapabilityConfig {
5454 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5455 config: serde_json::json!({
5456 "targets": [{
5457 "id": "aws_operator",
5458 "name": "AWS Operator",
5459 "agent_id": agent_id,
5460 "harness_id": harness_id
5461 }]
5462 }),
5463 },
5464 ];
5465
5466 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5467 let spawn_agent_defs: Vec<_> = collected
5468 .tool_definitions
5469 .iter()
5470 .filter(|tool| tool.name() == "spawn_agent")
5471 .collect();
5472
5473 assert_eq!(spawn_agent_defs.len(), 1);
5474 let schema = spawn_agent_defs[0].parameters();
5475 assert_eq!(
5476 schema["properties"]["target"]["properties"]["type"]["enum"],
5477 serde_json::json!(["subagent", "agent"])
5478 );
5479 assert!(schema.get("oneOf").is_none());
5482 assert!(schema.get("anyOf").is_none());
5483 assert!(schema.get("allOf").is_none());
5484 assert_eq!(
5485 schema["required"],
5486 serde_json::json!(["name", "instructions", "target"])
5487 );
5488 assert_eq!(
5489 schema["properties"]["target"]["oneOf"],
5490 serde_json::json!([
5491 {
5492 "properties": {"type": {"const": "subagent"}}
5493 },
5494 {
5495 "properties": {"type": {"const": "agent"}},
5496 "required": ["type", "id"]
5497 }
5498 ])
5499 );
5500 }
5501
5502 #[cfg(feature = "a2a")]
5503 #[tokio::test]
5504 async fn test_spawn_agent_dispatcher_includes_external_a2a_provider() {
5505 let mut registry = CapabilityRegistry::new();
5506 registry.register(SubagentCapability);
5507 registry.register(A2aAgentDelegationCapability);
5508
5509 let configs = vec![
5510 AgentCapabilityConfig {
5511 capability_ref: CapabilityId::new(SUBAGENTS_CAPABILITY_ID),
5512 config: serde_json::json!({}),
5513 },
5514 AgentCapabilityConfig {
5515 capability_ref: CapabilityId::new(A2A_AGENT_DELEGATION_CAPABILITY_ID),
5516 config: serde_json::json!({
5517 "agents": [{
5518 "id": "local_app",
5519 "name": "Local App",
5520 "base_url": "https://example.com"
5521 }]
5522 }),
5523 },
5524 ];
5525
5526 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5527 let spawn_agent_defs: Vec<_> = collected
5528 .tool_definitions
5529 .iter()
5530 .filter(|tool| tool.name() == "spawn_agent")
5531 .collect();
5532
5533 assert_eq!(spawn_agent_defs.len(), 1);
5534 assert_eq!(
5535 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5536 serde_json::json!(["subagent", "external_a2a"])
5537 );
5538 assert_eq!(
5539 spawn_agent_defs[0].parameters()["properties"]["mode"]["enum"],
5540 serde_json::json!(["background", "foreground"])
5541 );
5542 assert!(
5543 !spawn_agent_defs[0].parameters()["properties"]["mode"]["description"]
5544 .as_str()
5545 .expect("mode description")
5546 .contains("wait")
5547 );
5548 let schema = spawn_agent_defs[0].parameters();
5549 assert!(schema.get("oneOf").is_none());
5550 assert_eq!(
5554 schema["required"],
5555 serde_json::json!(["name", "instructions", "target"])
5556 );
5557 assert_eq!(
5558 schema["properties"]["target"]["oneOf"],
5559 serde_json::json!([
5560 {
5561 "properties": {"type": {"const": "subagent"}}
5562 },
5563 {
5564 "properties": {"type": {"const": "external_a2a"}},
5565 "anyOf": [
5566 {"required": ["id"]},
5567 {"required": ["external_agent_id"]}
5568 ]
5569 }
5570 ])
5571 );
5572 }
5573
5574 struct ExistingSpawnAgentCapability;
5575
5576 impl Capability for ExistingSpawnAgentCapability {
5577 fn id(&self) -> &str {
5578 "existing_spawn_agent"
5579 }
5580
5581 fn name(&self) -> &str {
5582 "Existing Spawn Agent"
5583 }
5584
5585 fn description(&self) -> &str {
5586 "Test capability that already owns spawn_agent"
5587 }
5588
5589 fn tools(&self) -> Vec<Box<dyn Tool>> {
5590 vec![Box::new(ExistingSpawnAgentTool)]
5591 }
5592 }
5593
5594 struct ExistingSpawnAgentTool;
5595
5596 #[async_trait]
5597 impl Tool for ExistingSpawnAgentTool {
5598 fn name(&self) -> &str {
5599 "spawn_agent"
5600 }
5601
5602 fn description(&self) -> &str {
5603 "Existing spawn_agent test tool"
5604 }
5605
5606 fn parameters_schema(&self) -> serde_json::Value {
5607 serde_json::json!({
5608 "type": "object",
5609 "properties": {
5610 "target": {
5611 "type": "object",
5612 "properties": {
5613 "type": {"type": "string", "enum": ["external_a2a"]}
5614 },
5615 "required": ["type"]
5616 }
5617 },
5618 "required": ["target"]
5619 })
5620 }
5621
5622 async fn execute(
5623 &self,
5624 _arguments: serde_json::Value,
5625 ) -> crate::tools::ToolExecutionResult {
5626 crate::tools::ToolExecutionResult::success(serde_json::json!({"ok": true}))
5627 }
5628 }
5629
5630 #[tokio::test]
5631 async fn test_subagents_do_not_shadow_existing_spawn_agent_provider() {
5632 let mut registry = CapabilityRegistry::new();
5633 registry.register(SubagentCapability);
5634 registry.register(ExistingSpawnAgentCapability);
5635
5636 let collected = collect_capabilities(
5637 &[
5638 SUBAGENTS_CAPABILITY_ID.to_string(),
5639 "existing_spawn_agent".to_string(),
5640 ],
5641 ®istry,
5642 &test_ctx(),
5643 )
5644 .await;
5645
5646 let spawn_agent_defs: Vec<_> = collected
5647 .tool_definitions
5648 .iter()
5649 .filter(|tool| tool.name() == "spawn_agent")
5650 .collect();
5651 assert_eq!(spawn_agent_defs.len(), 1);
5652 assert_eq!(
5653 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5654 serde_json::json!(["external_a2a"])
5655 );
5656 }
5657
5658 #[tokio::test]
5659 async fn test_agent_handoff_does_not_shadow_existing_spawn_agent_provider() {
5660 let mut registry = CapabilityRegistry::new();
5661 registry.register(AgentHandoffCapability);
5662 registry.register(ExistingSpawnAgentCapability);
5663
5664 let agent_id = crate::typed_id::AgentId::new();
5665 let harness_id = crate::typed_id::HarnessId::new();
5666 let configs = vec![
5667 AgentCapabilityConfig {
5668 capability_ref: CapabilityId::new(AGENT_HANDOFF_CAPABILITY_ID),
5669 config: serde_json::json!({
5670 "targets": [{
5671 "id": "aws_operator",
5672 "name": "AWS Operator",
5673 "agent_id": agent_id,
5674 "harness_id": harness_id
5675 }]
5676 }),
5677 },
5678 AgentCapabilityConfig {
5679 capability_ref: CapabilityId::new("existing_spawn_agent"),
5680 config: serde_json::json!({}),
5681 },
5682 ];
5683
5684 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
5685
5686 let spawn_agent_defs: Vec<_> = collected
5687 .tool_definitions
5688 .iter()
5689 .filter(|tool| tool.name() == "spawn_agent")
5690 .collect();
5691 assert_eq!(spawn_agent_defs.len(), 1);
5692 assert_eq!(
5693 spawn_agent_defs[0].parameters()["properties"]["target"]["properties"]["type"]["enum"],
5694 serde_json::json!(["external_a2a"])
5695 );
5696 }
5697
5698 #[tokio::test]
5702 async fn test_background_execution_explicit_selection_is_idempotent() {
5703 let registry = CapabilityRegistry::with_builtins();
5704 let collected = collect_capabilities(
5705 &[
5706 "bashkit_shell".to_string(),
5707 BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
5708 ],
5709 ®istry,
5710 &test_ctx(),
5711 )
5712 .await;
5713
5714 let spawn_background_count = collected
5715 .tool_definitions
5716 .iter()
5717 .filter(|t| t.name() == "spawn_background")
5718 .count();
5719 assert_eq!(
5720 spawn_background_count, 1,
5721 "spawn_background must appear exactly once even when \
5722 background_execution is selected explicitly alongside a \
5723 background-capable tool"
5724 );
5725 let applied_count = collected
5726 .applied_ids
5727 .iter()
5728 .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
5729 .count();
5730 assert_eq!(
5731 applied_count, 1,
5732 "background_execution must appear exactly once in applied_ids"
5733 );
5734 }
5735
5736 #[test]
5741 fn test_defaults_do_not_include_spawn_background() {
5742 let registry = crate::ToolRegistry::with_defaults();
5743 assert!(
5744 !registry.has("spawn_background"),
5745 "with_defaults() must not include 'spawn_background' — it comes \
5746 from the background_execution capability (EVE-501)"
5747 );
5748 }
5749
5750 #[test]
5755 fn test_capability_features_default_empty() {
5756 let registry = CapabilityRegistry::with_builtins();
5757
5758 let noop = registry.get("noop").unwrap();
5760 assert!(noop.features().is_empty());
5761
5762 let current_time = registry.get("current_time").unwrap();
5763 assert!(current_time.features().is_empty());
5764 }
5765
5766 #[test]
5767 fn test_file_system_capability_features() {
5768 let registry = CapabilityRegistry::with_builtins();
5769
5770 let fs = registry.get("session_file_system").unwrap();
5771 assert_eq!(fs.features(), vec!["file_system"]);
5772 }
5773
5774 #[test]
5775 fn test_bashkit_shell_capability_features() {
5776 let registry = CapabilityRegistry::with_builtins();
5777
5778 let bash = registry.get("bashkit_shell").unwrap();
5779 assert_eq!(bash.features(), vec!["file_system"]);
5780 }
5781
5782 #[test]
5783 fn test_alias_resolves_to_canonical_capability() {
5784 let registry = CapabilityRegistry::with_builtins();
5785
5786 let via_alias = registry.get("virtual_bash").unwrap();
5788 assert_eq!(via_alias.id(), "bashkit_shell");
5789 assert!(registry.has("virtual_bash"));
5790 assert_eq!(registry.canonical_id("virtual_bash"), Some("bashkit_shell"));
5791 assert_eq!(
5792 registry.canonical_id("bashkit_shell"),
5793 Some("bashkit_shell")
5794 );
5795 assert_eq!(registry.canonical_id("nonexistent"), None);
5796 }
5797
5798 #[test]
5799 fn test_alias_dedupes_with_canonical_in_dependency_resolution() {
5800 let registry = CapabilityRegistry::with_builtins();
5801
5802 let resolved = resolve_dependencies(
5805 &["virtual_bash".to_string(), "bashkit_shell".to_string()],
5806 ®istry,
5807 )
5808 .unwrap();
5809 let bash_ids: Vec<_> = resolved
5810 .resolved_ids
5811 .iter()
5812 .filter(|id| id.as_str() == "bashkit_shell" || id.as_str() == "virtual_bash")
5813 .collect();
5814 assert_eq!(bash_ids, vec!["bashkit_shell"]);
5815 assert!(
5817 !resolved
5818 .added_as_dependencies
5819 .contains(&"bashkit_shell".to_string())
5820 );
5821 }
5822
5823 #[test]
5824 fn test_alias_preserves_explicit_config_in_resolution() {
5825 let registry = CapabilityRegistry::with_builtins();
5826
5827 let configs = vec![AgentCapabilityConfig::with_config(
5828 "virtual_bash".to_string(),
5829 serde_json::json!({"key": "value"}),
5830 )];
5831 let resolved = resolve_capability_configs(&configs, ®istry).unwrap();
5832 let bash = resolved
5833 .iter()
5834 .find(|c| c.capability_id() == "bashkit_shell")
5835 .expect("alias must resolve to canonical bashkit_shell config");
5836 assert_eq!(bash.config, serde_json::json!({"key": "value"}));
5837 }
5838
5839 #[test]
5840 fn test_unregister_by_alias_removes_capability_and_aliases() {
5841 let mut registry = CapabilityRegistry::with_builtins();
5842
5843 assert!(registry.unregister("virtual_bash").is_some());
5844 assert!(!registry.has("bashkit_shell"));
5845 assert!(!registry.has("virtual_bash"));
5846 }
5847
5848 #[test]
5849 fn test_session_storage_capability_features() {
5850 let registry = CapabilityRegistry::with_builtins();
5851
5852 let storage = registry.get("session_storage").unwrap();
5853 let features = storage.features();
5854 assert!(features.contains(&"secrets"));
5855 assert!(features.contains(&"key_value"));
5856 }
5857
5858 #[test]
5859 fn test_session_schedule_capability_features() {
5860 let registry = CapabilityRegistry::with_builtins();
5861
5862 let schedule = registry.get("session_schedule").unwrap();
5863 assert_eq!(schedule.features(), vec!["schedules"]);
5864 }
5865
5866 #[test]
5867 fn test_session_sql_database_capability_features() {
5868 let registry = CapabilityRegistry::with_builtins();
5869
5870 let sql = registry.get("session_sql_database").unwrap();
5871 assert_eq!(sql.features(), vec!["sql_database"]);
5872 }
5873
5874 #[test]
5875 fn test_sample_data_capability_features() {
5876 let registry = CapabilityRegistry::with_builtins();
5877
5878 let sample = registry.get("sample_data").unwrap();
5879 assert_eq!(sample.features(), vec!["file_system"]);
5880 }
5881
5882 #[test]
5883 fn test_compute_features_empty() {
5884 let registry = CapabilityRegistry::with_builtins();
5885
5886 let features = compute_features(&[], ®istry);
5887 assert!(features.is_empty());
5888 }
5889
5890 #[test]
5891 fn test_compute_features_single_capability() {
5892 let registry = CapabilityRegistry::with_builtins();
5893
5894 let features = compute_features(&["session_schedule".to_string()], ®istry);
5895 assert_eq!(features, vec!["schedules"]);
5896 }
5897
5898 #[test]
5899 fn test_compute_features_multiple_capabilities() {
5900 let registry = CapabilityRegistry::with_builtins();
5901
5902 let features = compute_features(
5903 &[
5904 "session_file_system".to_string(),
5905 "session_storage".to_string(),
5906 "session_schedule".to_string(),
5907 ],
5908 ®istry,
5909 );
5910 assert!(features.contains(&"file_system".to_string()));
5911 assert!(features.contains(&"secrets".to_string()));
5912 assert!(features.contains(&"key_value".to_string()));
5913 assert!(features.contains(&"schedules".to_string()));
5914 }
5915
5916 #[test]
5917 fn test_compute_features_deduplicates() {
5918 let registry = CapabilityRegistry::with_builtins();
5919
5920 let features = compute_features(
5922 &[
5923 "session_file_system".to_string(),
5924 "bashkit_shell".to_string(),
5925 ],
5926 ®istry,
5927 );
5928 let file_system_count = features.iter().filter(|f| *f == "file_system").count();
5929 assert_eq!(file_system_count, 1, "file_system should appear only once");
5930 }
5931
5932 #[test]
5933 fn test_compute_features_includes_dependency_features() {
5934 let registry = CapabilityRegistry::with_builtins();
5935
5936 let features = compute_features(&["bashkit_shell".to_string()], ®istry);
5938 assert!(features.contains(&"file_system".to_string()));
5939 }
5940
5941 #[test]
5942 fn test_compute_features_generic_harness_set() {
5943 let registry = CapabilityRegistry::with_builtins();
5944
5945 let features = compute_features(
5947 &[
5948 "session_file_system".to_string(),
5949 "bashkit_shell".to_string(),
5950 "session_storage".to_string(),
5951 "session".to_string(),
5952 "session_schedule".to_string(),
5953 ],
5954 ®istry,
5955 );
5956 assert!(features.contains(&"file_system".to_string()));
5957 assert!(features.contains(&"secrets".to_string()));
5958 assert!(features.contains(&"key_value".to_string()));
5959 assert!(features.contains(&"schedules".to_string()));
5960 }
5961
5962 #[test]
5963 fn test_compute_features_unknown_capability_ignored() {
5964 let registry = CapabilityRegistry::with_builtins();
5965
5966 let features = compute_features(
5967 &["unknown_cap".to_string(), "session_schedule".to_string()],
5968 ®istry,
5969 );
5970 assert_eq!(features, vec!["schedules"]);
5971 }
5972
5973 #[test]
5974 fn test_risk_level_ordering() {
5975 assert!(RiskLevel::Low < RiskLevel::Medium);
5976 assert!(RiskLevel::Medium < RiskLevel::High);
5977 }
5978
5979 #[test]
5980 fn test_risk_level_serde_roundtrip() {
5981 let high = RiskLevel::High;
5982 let json = serde_json::to_string(&high).unwrap();
5983 assert_eq!(json, "\"high\"");
5984 let back: RiskLevel = serde_json::from_str(&json).unwrap();
5985 assert_eq!(back, RiskLevel::High);
5986 }
5987
5988 #[test]
5989 fn test_capability_risk_levels() {
5990 let registry = CapabilityRegistry::with_builtins();
5991
5992 let bash = registry.get("bashkit_shell").unwrap();
5994 assert_eq!(bash.risk_level(), RiskLevel::High);
5995
5996 let fetch = registry.get("web_fetch").unwrap();
5998 assert_eq!(fetch.risk_level(), RiskLevel::High);
5999
6000 let noop = registry.get("noop").unwrap();
6002 assert_eq!(noop.risk_level(), RiskLevel::Low);
6003 }
6004
6005 #[tokio::test]
6010 async fn test_apply_capabilities_openai_tool_search() {
6011 let registry = CapabilityRegistry::with_builtins();
6012 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6013
6014 let applied = apply_capabilities(
6015 base_runtime_agent.clone(),
6016 &["openai_tool_search".to_string()],
6017 ®istry,
6018 &test_ctx(),
6019 )
6020 .await;
6021
6022 assert_eq!(
6024 applied.runtime_agent.system_prompt,
6025 base_runtime_agent.system_prompt
6026 );
6027 assert!(applied.tool_registry.is_empty());
6028 assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
6029
6030 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6032 assert!(ts.enabled);
6033 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6034 }
6035
6036 #[tokio::test]
6037 async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
6038 let registry = CapabilityRegistry::with_builtins();
6039 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6040
6041 let applied = apply_capabilities(
6042 base_runtime_agent,
6043 &[
6044 "current_time".to_string(),
6045 "openai_tool_search".to_string(),
6046 "test_math".to_string(),
6047 ],
6048 ®istry,
6049 &test_ctx(),
6050 )
6051 .await;
6052
6053 assert!(applied.tool_registry.has("get_current_time"));
6055 assert!(applied.tool_registry.has("add"));
6056 assert!(applied.tool_registry.has("subtract"));
6057 assert!(applied.tool_registry.has("multiply"));
6058 assert!(applied.tool_registry.has("divide"));
6059
6060 let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
6062 assert!(ts.enabled);
6063 assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
6064 }
6065
6066 #[tokio::test]
6067 async fn test_collect_capabilities_tool_search_custom_threshold() {
6068 let registry = CapabilityRegistry::with_builtins();
6069
6070 let configs = vec![AgentCapabilityConfig {
6071 capability_ref: CapabilityId::new("openai_tool_search"),
6072 config: serde_json::json!({"threshold": 5}),
6073 }];
6074
6075 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6076
6077 let ts = collected.tool_search.as_ref().unwrap();
6078 assert!(ts.enabled);
6079 assert_eq!(ts.threshold, 5);
6080 }
6081
6082 #[tokio::test]
6083 async fn test_collect_capabilities_auto_tool_search_resolves_to_generic_off_native() {
6084 let registry = CapabilityRegistry::with_builtins();
6085
6086 let configs = vec![
6087 AgentCapabilityConfig {
6088 capability_ref: CapabilityId::new("auto_tool_search"),
6089 config: serde_json::json!({"threshold": 2}),
6090 },
6091 AgentCapabilityConfig {
6092 capability_ref: CapabilityId::new("test_math"),
6093 config: serde_json::json!({}),
6094 },
6095 ];
6096
6097 let ctx = test_ctx().with_model("claude-3-5-haiku");
6101 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6102
6103 assert!(
6104 collected.tool_search.is_none(),
6105 "auto_tool_search must not set a hosted config on a non-native model"
6106 );
6107 assert!(
6108 collected
6109 .tools
6110 .iter()
6111 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6112 "auto_tool_search must contribute the client-side tool_search tool"
6113 );
6114 assert!(
6115 !collected.tool_definition_hooks.is_empty(),
6116 "auto_tool_search must contribute a client-side deferral hook"
6117 );
6118
6119 let mut transformed = collected.tool_definitions.clone();
6120 for hook in &collected.tool_definition_hooks {
6121 transformed = hook.transform(transformed);
6122 }
6123 let add_tool = transformed
6124 .iter()
6125 .find(|tool| tool.name() == "add")
6126 .expect("test_math contributes add");
6127 assert!(
6128 add_tool.parameters().get("properties").is_none(),
6129 "generic auto_tool_search must honor the configured threshold"
6130 );
6131 }
6132
6133 #[tokio::test]
6134 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_native() {
6135 let registry = CapabilityRegistry::with_builtins();
6136
6137 let configs = vec![AgentCapabilityConfig {
6138 capability_ref: CapabilityId::new("auto_tool_search"),
6139 config: serde_json::json!({"threshold": 7}),
6140 }];
6141
6142 let ctx = test_ctx().with_model("gpt-5.4");
6145 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6146
6147 let ts = collected
6148 .tool_search
6149 .as_ref()
6150 .expect("auto_tool_search must set a hosted config on a native model");
6151 assert!(ts.enabled);
6152 assert_eq!(ts.threshold, 7);
6153 assert!(
6154 !collected
6155 .tools
6156 .iter()
6157 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6158 "hosted mechanism must not contribute the client-side tool_search tool"
6159 );
6160 assert!(
6161 collected.tool_definition_hooks.is_empty(),
6162 "hosted mechanism must not contribute a client-side deferral hook"
6163 );
6164 }
6165
6166 #[tokio::test]
6167 async fn test_collect_capabilities_auto_tool_search_resolves_to_hosted_on_anthropic() {
6168 let registry = CapabilityRegistry::with_builtins();
6169
6170 let configs = vec![AgentCapabilityConfig {
6171 capability_ref: CapabilityId::new("auto_tool_search"),
6172 config: serde_json::json!({"threshold": 9}),
6173 }];
6174
6175 let ctx = test_ctx().with_model("claude-opus-4-8");
6178 let collected = collect_capabilities_with_configs(&configs, ®istry, &ctx).await;
6179
6180 let ts = collected
6181 .tool_search
6182 .as_ref()
6183 .expect("auto_tool_search must set a hosted config on a native Claude model");
6184 assert!(ts.enabled);
6185 assert_eq!(ts.threshold, 9);
6186 assert!(
6187 !collected
6188 .tools
6189 .iter()
6190 .any(|t| t.name() == TOOL_SEARCH_TOOL_NAME),
6191 "hosted mechanism must not contribute the client-side tool_search tool"
6192 );
6193 assert!(
6194 collected.tool_definition_hooks.is_empty(),
6195 "hosted mechanism must not contribute a client-side deferral hook"
6196 );
6197 }
6198
6199 #[tokio::test]
6200 async fn test_collect_capabilities_no_tool_search_without_capability() {
6201 let registry = CapabilityRegistry::with_builtins();
6202
6203 let configs = vec![AgentCapabilityConfig {
6204 capability_ref: CapabilityId::new("current_time"),
6205 config: serde_json::json!({}),
6206 }];
6207
6208 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6209
6210 assert!(collected.tool_search.is_none());
6211 }
6212
6213 #[tokio::test]
6214 async fn test_collect_capabilities_tool_search_category_propagation() {
6215 let registry = CapabilityRegistry::with_builtins();
6216
6217 let configs = vec![
6219 AgentCapabilityConfig {
6220 capability_ref: CapabilityId::new("test_math"),
6221 config: serde_json::json!({}),
6222 },
6223 AgentCapabilityConfig {
6224 capability_ref: CapabilityId::new("openai_tool_search"),
6225 config: serde_json::json!({}),
6226 },
6227 ];
6228
6229 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6230
6231 assert!(collected.tool_search.is_some());
6233
6234 for tool_def in &collected.tool_definitions {
6236 if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
6238 assert!(
6239 tool_def.category().is_some(),
6240 "Tool {} should have a category from its capability",
6241 tool_def.name()
6242 );
6243 }
6244 }
6245 }
6246
6247 #[tokio::test]
6248 async fn test_apply_capabilities_prompt_caching() {
6249 let registry = CapabilityRegistry::with_builtins();
6250 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6251
6252 let applied = apply_capabilities(
6253 base_runtime_agent.clone(),
6254 &["prompt_caching".to_string()],
6255 ®istry,
6256 &test_ctx(),
6257 )
6258 .await;
6259
6260 assert_eq!(
6261 applied.runtime_agent.system_prompt,
6262 base_runtime_agent.system_prompt
6263 );
6264 assert!(applied.tool_registry.is_empty());
6265 assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
6266
6267 let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
6268 assert!(prompt_cache.enabled);
6269 assert_eq!(
6270 prompt_cache.strategy,
6271 crate::driver_registry::PromptCacheStrategy::Auto
6272 );
6273 assert!(prompt_cache.gemini_cached_content.is_none());
6274 }
6275
6276 #[tokio::test]
6277 async fn test_apply_capabilities_openrouter_server_tools() {
6278 let registry = CapabilityRegistry::with_builtins();
6279 let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
6280
6281 let configs = vec![AgentCapabilityConfig {
6282 capability_ref: CapabilityId::new("openrouter_server_tools"),
6283 config: serde_json::json!({
6284 "tools": ["web_search", "datetime"],
6285 "web_search_max_results": 4,
6286 }),
6287 }];
6288
6289 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6290 let routing = collected
6291 .openrouter_routing
6292 .as_ref()
6293 .expect("server tools produce routing config");
6294 let kinds: Vec<_> = routing.server_tools.iter().map(|t| t.kind).collect();
6295 assert_eq!(
6296 kinds,
6297 vec![
6298 crate::driver_registry::OpenRouterServerToolKind::WebSearch,
6299 crate::driver_registry::OpenRouterServerToolKind::Datetime,
6300 ]
6301 );
6302
6303 let applied = apply_capabilities(
6306 base_runtime_agent,
6307 &["openrouter_server_tools".to_string()],
6308 ®istry,
6309 &test_ctx(),
6310 )
6311 .await;
6312 assert!(applied.tool_registry.is_empty());
6313 assert!(applied.runtime_agent.openrouter_routing.is_none());
6314 }
6315
6316 #[tokio::test]
6317 async fn test_collect_capabilities_prompt_caching_custom_strategy() {
6318 let registry = CapabilityRegistry::with_builtins();
6319
6320 let configs = vec![AgentCapabilityConfig {
6321 capability_ref: CapabilityId::new("prompt_caching"),
6322 config: serde_json::json!({"strategy": "auto"}),
6323 }];
6324
6325 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6326
6327 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6328 assert!(prompt_cache.enabled);
6329 assert_eq!(
6330 prompt_cache.strategy,
6331 crate::driver_registry::PromptCacheStrategy::Auto
6332 );
6333 assert!(prompt_cache.gemini_cached_content.is_none());
6334 }
6335
6336 #[tokio::test]
6337 async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
6338 let registry = CapabilityRegistry::with_builtins();
6339
6340 let configs = vec![AgentCapabilityConfig {
6341 capability_ref: CapabilityId::new("prompt_caching"),
6342 config: serde_json::json!({
6343 "strategy": "auto",
6344 "gemini_cached_content": "cachedContents/demo-cache"
6345 }),
6346 }];
6347
6348 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6349
6350 let prompt_cache = collected.prompt_cache.as_ref().unwrap();
6351 assert_eq!(
6352 prompt_cache.gemini_cached_content.as_deref(),
6353 Some("cachedContents/demo-cache")
6354 );
6355 }
6356
6357 #[tokio::test]
6358 async fn test_collect_capabilities_parallel_tool_calls_modes() {
6359 let registry = CapabilityRegistry::with_builtins();
6360
6361 let collected = collect_capabilities_with_configs(
6363 &[AgentCapabilityConfig::new("parallel_tool_calls")],
6364 ®istry,
6365 &test_ctx(),
6366 )
6367 .await;
6368 assert_eq!(collected.parallel_tool_calls, Some(true));
6369
6370 let collected = collect_capabilities_with_configs(
6372 &[AgentCapabilityConfig {
6373 capability_ref: CapabilityId::new("parallel_tool_calls"),
6374 config: serde_json::json!({"mode": "avoid"}),
6375 }],
6376 ®istry,
6377 &test_ctx(),
6378 )
6379 .await;
6380 assert_eq!(collected.parallel_tool_calls, Some(false));
6381
6382 let collected = collect_capabilities_with_configs(
6384 &[AgentCapabilityConfig {
6385 capability_ref: CapabilityId::new("parallel_tool_calls"),
6386 config: serde_json::json!({"mode": "none"}),
6387 }],
6388 ®istry,
6389 &test_ctx(),
6390 )
6391 .await;
6392 assert_eq!(collected.parallel_tool_calls, None);
6393
6394 let collected = collect_capabilities_with_configs(&[], ®istry, &test_ctx()).await;
6396 assert_eq!(collected.parallel_tool_calls, None);
6397 }
6398
6399 #[tokio::test]
6400 async fn test_apply_capabilities_parallel_tool_calls_precedence() {
6401 let registry = CapabilityRegistry::with_builtins();
6402
6403 let applied = apply_capabilities(
6405 RuntimeAgent::new("p", "gpt-5.2"),
6406 &["parallel_tool_calls".to_string()],
6407 ®istry,
6408 &test_ctx(),
6409 )
6410 .await;
6411 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(true));
6412
6413 let mut base = RuntimeAgent::new("p", "gpt-5.2");
6415 base.parallel_tool_calls = Some(false);
6416 let applied = apply_capabilities(
6417 base,
6418 &["parallel_tool_calls".to_string()],
6419 ®istry,
6420 &test_ctx(),
6421 )
6422 .await;
6423 assert_eq!(applied.runtime_agent.parallel_tool_calls, Some(false));
6424 }
6425
6426 struct SkillContributingCapability;
6431
6432 impl Capability for SkillContributingCapability {
6433 fn id(&self) -> &str {
6434 "contributes_skills"
6435 }
6436 fn name(&self) -> &str {
6437 "Contributes Skills"
6438 }
6439 fn description(&self) -> &str {
6440 "Test capability that contributes skills."
6441 }
6442 fn contribute_skills(&self) -> Vec<SkillContribution> {
6443 vec![
6444 SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
6445 .with_files(vec![(
6446 "scripts/a.sh".to_string(),
6447 "#!/bin/sh\necho a\n".to_string(),
6448 )]),
6449 SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
6450 .with_user_invocable(false),
6451 ]
6452 }
6453 }
6454
6455 fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
6456 match &entries.get("SKILL.md").expect("SKILL.md missing").source {
6457 MountSource::InlineFile { content, .. } => content.as_str(),
6458 _ => panic!("Expected InlineFile for SKILL.md"),
6459 }
6460 }
6461
6462 #[tokio::test]
6463 async fn test_contribute_skills_normalized_to_mounts() {
6464 let mut registry = CapabilityRegistry::new();
6465 registry.register(SkillContributingCapability);
6466
6467 let configs = vec![AgentCapabilityConfig {
6468 capability_ref: CapabilityId::new("contributes_skills"),
6469 config: serde_json::json!({}),
6470 }];
6471
6472 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6473
6474 let skill_mounts: Vec<_> = collected
6475 .mounts
6476 .iter()
6477 .filter(|m| m.path.starts_with("/.agents/skills/"))
6478 .collect();
6479 assert_eq!(skill_mounts.len(), 2);
6480
6481 for m in &skill_mounts {
6484 assert!(m.is_readonly());
6485 assert_eq!(m.capability_id, "contributes_skills");
6486 }
6487
6488 let alpha = skill_mounts
6489 .iter()
6490 .find(|m| m.path == "/.agents/skills/alpha-skill")
6491 .expect("alpha-skill mount missing");
6492 match &alpha.source {
6493 MountSource::InlineDirectory { entries } => {
6494 assert!(entries.contains_key("SKILL.md"));
6495 assert!(entries.contains_key("scripts/a.sh"));
6496 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6497 assert_eq!(parsed.name, "alpha-skill");
6498 assert!(parsed.user_invocable);
6499 }
6500 _ => panic!("Expected InlineDirectory"),
6501 }
6502
6503 let beta = skill_mounts
6504 .iter()
6505 .find(|m| m.path == "/.agents/skills/beta-skill")
6506 .expect("beta-skill mount missing");
6507 match &beta.source {
6508 MountSource::InlineDirectory { entries } => {
6509 let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
6510 assert!(!parsed.user_invocable);
6511 }
6512 _ => panic!("Expected InlineDirectory"),
6513 }
6514 }
6515
6516 #[tokio::test]
6517 async fn test_contribute_skills_default_empty() {
6518 let mut registry = CapabilityRegistry::new();
6521 registry.register(FilterTestCapability { priority: 0 });
6522
6523 let configs = vec![AgentCapabilityConfig {
6524 capability_ref: CapabilityId::new("filter_test"),
6525 config: serde_json::json!({}),
6526 }];
6527
6528 let collected = collect_capabilities_with_configs(&configs, ®istry, &test_ctx()).await;
6529 assert!(
6530 collected
6531 .mounts
6532 .iter()
6533 .all(|m| !m.path.starts_with("/.agents/skills/"))
6534 );
6535 }
6536
6537 struct LocalizedCapability;
6538
6539 impl Capability for LocalizedCapability {
6540 fn id(&self) -> &str {
6541 "localized"
6542 }
6543 fn name(&self) -> &str {
6544 "Localized"
6545 }
6546 fn description(&self) -> &str {
6547 "English description"
6548 }
6549 fn localizations(&self) -> Vec<CapabilityLocalization> {
6550 vec![
6551 CapabilityLocalization {
6552 locale: "en",
6553 name: None,
6554 description: None,
6555 config_description: Some("Controls things."),
6556 config_overlay: None,
6557 },
6558 CapabilityLocalization {
6559 locale: "uk",
6560 name: Some("Локалізована"),
6561 description: Some("Український опис"),
6562 config_description: Some("Керує налаштуваннями."),
6563 config_overlay: None,
6564 },
6565 ]
6566 }
6567 }
6568
6569 #[test]
6570 fn localized_name_falls_back_exact_language_then_base() {
6571 let cap = LocalizedCapability;
6572 assert_eq!(cap.localized_name(Some("uk-UA")), "Локалізована");
6574 assert_eq!(cap.localized_name(Some("uk")), "Локалізована");
6575 assert_eq!(cap.localized_name(Some("uk_UA")), "Локалізована");
6577 assert_eq!(cap.localized_name(Some("fr-FR")), "Localized");
6579 assert_eq!(cap.localized_name(None), "Localized");
6580 assert_eq!(cap.localized_description(Some("uk")), "Український опис");
6581 assert_eq!(cap.localized_description(Some("de")), "English description");
6582 }
6583
6584 #[test]
6585 fn describe_schema_resolves_config_description_per_locale() {
6586 let cap = LocalizedCapability;
6587 assert_eq!(
6588 cap.describe_schema(Some("uk-UA")).as_deref(),
6589 Some("Керує налаштуваннями.")
6590 );
6591 assert_eq!(
6593 cap.describe_schema(Some("pl")).as_deref(),
6594 Some("Controls things.")
6595 );
6596 assert_eq!(
6597 cap.describe_schema(None).as_deref(),
6598 Some("Controls things.")
6599 );
6600 assert_eq!(NoopCapability.describe_schema(Some("uk")), None);
6602 }
6603}