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