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