Skip to main content

everruns_core/capabilities/
mod.rs

1//! Capabilities Module for Agent Loop
2//!
3//! This module provides the capabilities abstraction that allows composing
4//! agent functionality through modular units. Each capability can contribute:
5//! - System prompt additions
6//! - Tools for the agent
7//! - Behavior modifications (future)
8//!
9//! Design decisions:
10//! - Capabilities are defined via the Capability trait for flexibility
11//! - CapabilityRegistry holds all available capability implementations
12//! - apply_capabilities() merges capability contributions into RuntimeAgent
13//! - The agent-loop remains execution-focused; capabilities are applied before execution
14//! - System prompt sections use XML tags for clear boundaries between components.
15//!   This follows Anthropic's recommendation for multi-component prompts and reduces
16//!   misattribution between capability instructions, user-provided AGENTS.md, and the
17//!   agent's base system prompt. See specs/xml-prompt-formatting.md for rationale.
18//!
19//! Each capability is in its own file with collocated tools.
20
21use crate::command::{
22    CommandDescriptor, CommandExecutionContext, CommandResult, ExecuteCommandRequest,
23};
24use crate::deployment::DeploymentGrade;
25use crate::events::TokenUsage;
26use crate::mcp_server::{ScopedMcpServers, merge_scoped_mcp_servers};
27use crate::message::Message;
28use crate::message_filter::MessageFilterProvider;
29use crate::runtime_agent::RuntimeAgent;
30use crate::tool_types::{ToolCall, ToolDefinition};
31use crate::tools::{Tool, ToolRegistry};
32use crate::traits::SessionFileSystem;
33use crate::typed_id::SessionId;
34use async_trait::async_trait;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::sync::Arc;
38
39// ============================================================================
40// Integration Plugin System
41// ============================================================================
42
43/// Plugin registration point for external integration crates.
44///
45/// Integration crates use `inventory::submit!` to register their capabilities
46/// without requiring `everruns-core` to know about them at compile time.
47/// The `CapabilityRegistry::with_builtins_for_grade()` method iterates all
48/// registered plugins and includes those matching the current deployment grade.
49///
50/// # Example
51///
52/// ```ignore
53/// // In integrations/daytona/src/lib.rs:
54/// inventory::submit! {
55///     everruns_core::capabilities::IntegrationPlugin {
56///         experimental_only: false,
57///         feature_flag: None,
58///         factory: || Box::new(DaytonaCapability),
59///     }
60/// }
61/// ```
62pub struct IntegrationPlugin {
63    /// If true, only registered when `DeploymentGrade::experimental_features_enabled()` is true.
64    pub experimental_only: bool,
65    /// If set, only registered when the named internal feature flag is enabled.
66    /// Checked via `InternalFeatureFlags::is_enabled()` at registry build time.
67    pub feature_flag: Option<&'static str>,
68    /// Factory function that creates the capability instance.
69    pub factory: fn() -> Box<dyn Capability>,
70}
71
72inventory::collect!(IntegrationPlugin);
73
74// Re-export capability types from capability_types module
75pub use crate::capability_types::{
76    AgentCapabilityConfig, CapabilityId, CapabilityStatus, MountAccess, MountDirectoryBuilder,
77    MountEntry, MountPoint, MountSource,
78};
79
80// ============================================================================
81// Capability Modules
82// ============================================================================
83
84mod a2a_delegation;
85#[cfg(feature = "ui-capabilities")]
86mod a2ui;
87mod agent_handoff;
88mod agent_instructions;
89pub mod attach_skill;
90mod background_execution;
91mod btw;
92mod budgeting;
93pub mod compaction;
94mod current_time;
95mod data_knowledge;
96mod declarative;
97mod fake_aws;
98mod fake_crm;
99mod fake_financial;
100mod fake_warehouse;
101mod file_system;
102mod human_intent;
103mod infinity_context;
104mod knowledge_base;
105mod loop_detection;
106pub mod mcp;
107mod noop;
108mod openai_tool_search;
109#[cfg(feature = "ui-capabilities")]
110mod openui;
111mod parallel;
112pub mod persistent_memory;
113mod platform_management;
114mod prompt_caching;
115mod prompt_canary_guardrail;
116mod research;
117mod sample_data;
118mod self_budget;
119mod session;
120mod session_sandbox;
121mod session_schedule;
122mod session_sql_database;
123mod session_storage;
124mod skills;
125mod stateless_todo_list;
126mod subagents;
127mod system_commands;
128mod test_math;
129mod test_weather;
130mod tool_output_persistence;
131pub mod user_hooks;
132mod virtual_bash;
133mod web_fetch;
134mod workspace_volumes;
135
136// Re-export capabilities
137pub use a2a_delegation::{
138    A2A_AGENT_DELEGATION_CAPABILITY_ID, A2aAgentDelegationCapability, CancelAgentTool,
139    GetAgentRunsTool, MessageAgentTool, SpawnAgentTool, WaitAgentTool,
140};
141#[cfg(feature = "ui-capabilities")]
142pub use a2ui::{A2UI_CAPABILITY_ID, A2UiCapability};
143pub use agent_handoff::{
144    AGENT_HANDOFF_CAPABILITY_ID, AgentHandoffCapability, GetAgentHandoffsTool,
145    MessageAgentHandoffTool, StartAgentHandoffTool,
146};
147pub use agent_instructions::{
148    AGENT_INSTRUCTIONS_CAPABILITY_ID, AGENTS_MD_PATH, AgentInstructionsCapability,
149    AgentInstructionsConfig, DEFAULT_AGENT_INSTRUCTIONS_FILE, MAX_AGENT_INSTRUCTIONS_FILES,
150    MAX_AGENTS_MD_SIZE, format_agents_md_content, format_instruction_file_content,
151};
152pub use attach_skill::{
153    AttachSkillCapability, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH, SkillContribution,
154    SkillInstructions, SkillMeta, SkillSource, discover_skills_from_entries, is_skill_capability,
155    parse_skill_capability_id, reconstruct_skill_md, skill_capability_id,
156};
157pub use background_execution::{BACKGROUND_EXECUTION_CAPABILITY_ID, BackgroundExecutionCapability};
158pub use btw::{BTW_CAPABILITY_ID, BtwCapability};
159pub use budgeting::BudgetingCapability;
160pub use compaction::{
161    COMPACTION_CAPABILITY_ID, CompactionCapability, CompactionConfig, CompactionStep,
162    CompactionStrategy, CostControlConfig, CostControlMaskingResult, HierarchicalMemoryConfig,
163    MaskingSummaryFormat, MemoryTier, ObservationMaskingConfig, ObservationMaskingResult,
164    SessionCompactionMetrics, SummarizationConfig, aggressive_trim, apply_cost_control_masking,
165    apply_hierarchical_memory, apply_observation_masking, build_model_view_messages,
166    build_summarization_prompt, build_summary_message, classify_memory_tiers, estimate_tokens,
167    estimate_total_tokens, format_messages_for_summarization, should_compact_proactively,
168};
169pub use current_time::{CurrentTimeCapability, GetCurrentTimeTool};
170pub use data_knowledge::DataKnowledgeCapability;
171pub use declarative::{
172    DECLARATIVE_CAPABILITY_PREFIX, DeclarativeCapabilityDefinition, DeclarativeCapabilityFile,
173    DeclarativeCapabilitySkill, declarative_capability_id, declarative_capability_info,
174    hydrate_declarative_capability_config, is_declarative_capability,
175    parse_declarative_capability_id, validate_declarative_capability_definition,
176};
177pub use fake_aws::{
178    AwsCreateEc2InstanceTool, AwsCreateIamUserTool, AwsCreateRdsDatabaseTool,
179    AwsCreateS3BucketTool, AwsGetCloudWatchMetricsTool, AwsListEc2InstancesTool,
180    AwsListIamUsersTool, AwsListRdsDatabasesTool, AwsListS3BucketsTool, AwsListSecurityGroupsTool,
181    AwsStopEc2InstanceTool, FakeAwsCapability,
182};
183pub use fake_crm::{
184    CrmAddInteractionTool, CrmCreateCustomerTool, CrmCreateTicketTool, CrmGetCustomerTool,
185    CrmListCustomersTool, CrmListTicketsTool, CrmSearchCustomersTool, CrmUpdateTicketTool,
186    FakeCrmCapability,
187};
188pub use fake_financial::{
189    FakeFinancialCapability, FinanceCreateBudgetTool, FinanceCreateTransactionTool,
190    FinanceForecastCashFlowTool, FinanceGetBalanceTool, FinanceGetExpenseReportTool,
191    FinanceGetRevenueReportTool, FinanceListBudgetsTool, FinanceListTransactionsTool,
192};
193pub use fake_warehouse::{
194    FakeWarehouseCapability, WarehouseCreateInvoiceTool, WarehouseCreateOrderTool,
195    WarehouseCreateShipmentTool, WarehouseGetInventoryTool, WarehouseInventoryReportTool,
196    WarehouseListOrdersTool, WarehouseListShipmentsTool, WarehouseProcessReturnTool,
197    WarehouseUpdateInventoryTool, WarehouseUpdateShipmentStatusTool,
198};
199pub use file_system::{
200    DeleteFileTool, EditFileTool, FileSystemCapability, GrepFilesTool, ListDirectoryTool,
201    ReadFileTool, StatFileTool, WriteFileTool,
202};
203pub use human_intent::{HUMAN_INTENT_CAPABILITY_ID, HumanIntentCapability};
204pub use infinity_context::{
205    INFINITY_CONTEXT_CAPABILITY_ID, InfinityContextCapability, QueryHistoryTool,
206};
207pub use knowledge_base::{
208    KNOWLEDGE_BASE_CAPABILITY_ID, KnowledgeBaseCapability, KnowledgeBaseConfig,
209    validate_knowledge_base_config,
210};
211pub use loop_detection::LoopDetectionCapability;
212pub use mcp::{
213    MCP_CAPABILITY_PREFIX, McpCapability, is_mcp_capability, mcp_capability_id,
214    parse_mcp_capability_id,
215};
216pub use noop::NoopCapability;
217pub use openai_tool_search::{
218    DEFAULT_TOOL_SEARCH_THRESHOLD, OPENAI_TOOL_SEARCH_CAPABILITY_ID, OpenAiToolSearchCapability,
219};
220#[cfg(feature = "ui-capabilities")]
221pub use openui::{OPENUI_CAPABILITY_ID, OpenUiCapability};
222pub use parallel::ParallelCapability;
223pub use persistent_memory::{
224    ForgetTool, MEMORY_CAPABILITY_ID, MemoryCapability, MemoryConfig, RecallTool, RememberTool,
225};
226pub use platform_management::{
227    ManageAgentsTool, ManageHarnessesTool, ManageSessionsTool, PlatformManagementCapability,
228    ReadAgentsTool, ReadCapabilitiesTool, ReadHarnessesTool, ReadSessionsTool,
229    SessionReadMessagesTool, SessionReadResponseTool, SessionSendMessageTool,
230};
231pub use prompt_caching::{PROMPT_CACHING_CAPABILITY_ID, PromptCachingCapability};
232pub use prompt_canary_guardrail::{
233    DEFAULT_REPLACEMENT as PROMPT_CANARY_DEFAULT_REPLACEMENT,
234    PROMPT_CANARY_GUARDRAIL_CAPABILITY_ID, PromptCanaryGuardrailCapability,
235    REASON_CODE_SYSTEM_PROMPT_LEAK,
236};
237pub use research::ResearchCapability;
238pub use sample_data::SampleDataCapability;
239pub use self_budget::{SELF_BUDGET_CAPABILITY_ID, SelfBudgetCapability};
240pub use session::{GetSessionInfoTool, SessionCapability, WriteSessionTitleTool};
241pub use session_sandbox::{
242    SESSION_SANDBOX_CAPABILITY_ID, SandboxExecTool, SandboxManageTool, SandboxReadFileTool,
243    SandboxStatusTool, SandboxWriteFileTool, SessionSandboxCapability,
244};
245pub use session_schedule::{
246    CancelScheduleTool, CreateScheduleTool, ListSchedulesTool, SESSION_SCHEDULE_CAPABILITY_ID,
247    SessionScheduleCapability,
248};
249pub use session_sql_database::{
250    SessionSqlDatabaseCapability, SqlExecuteTool, SqlQueryTool, SqlSchemaTool,
251};
252pub use session_storage::{KvStoreTool, SecretStoreTool, SessionStorageCapability};
253pub use skills::{SKILLS_CAPABILITY_ID, SkillsCapability};
254pub use stateless_todo_list::{StatelessTodoListCapability, WriteTodosTool};
255pub use subagents::SubagentCapability;
256// Blueprint types are exported directly from the trait definitions above
257pub use system_commands::{SYSTEM_COMMANDS_CAPABILITY_ID, SystemCommandsCapability};
258pub use test_math::{AddTool, DivideTool, MultiplyTool, SubtractTool, TestMathCapability};
259pub use test_weather::{GetForecastTool, GetWeatherTool, TestWeatherCapability};
260pub use tool_output_persistence::{PersistOutputHook, ToolOutputPersistenceCapability};
261pub use user_hooks::UserHooksCapability;
262pub use virtual_bash::{BashTool, SessionFileSystemAdapter, VirtualBashCapability};
263pub use web_fetch::{
264    BotAuthPublicKey, WebFetchCapability, WebFetchTool, derive_bot_auth_public_key,
265};
266pub use workspace_volumes::{WORKSPACE_VOLUMES_CAPABILITY_ID, WorkspaceVolumesCapability};
267
268// ============================================================================
269// System Prompt Context
270// ============================================================================
271
272/// Context provided to capabilities when resolving dynamic system prompt contributions.
273///
274/// This gives capabilities access to session-specific resources (filesystem, etc.)
275/// so they can generate system prompt content at runtime rather than returning
276/// only static text.
277pub struct SystemPromptContext {
278    /// The current session ID
279    pub session_id: SessionId,
280    /// Optional locale for localized prompts and tool behavior.
281    pub locale: Option<String>,
282    /// Optional file store for reading session files (e.g., AGENTS.md)
283    pub file_store: Option<Arc<dyn SessionFileSystem>>,
284}
285
286impl SystemPromptContext {
287    /// Create context with no file store (for callers that don't need filesystem access)
288    pub fn without_file_store(session_id: SessionId) -> Self {
289        Self {
290            session_id,
291            locale: None,
292            file_store: None,
293        }
294    }
295}
296
297// ============================================================================
298// Capability Trait
299// ============================================================================
300
301/// Trait for implementing capabilities that extend agent functionality.
302///
303/// A capability can contribute:
304/// - System prompt additions (prepended to agent's system prompt)
305/// - Tools (added to agent's available tools)
306///
307/// # System Prompt Contributions
308///
309/// Capabilities provide system prompt content via `system_prompt_contribution()`.
310/// This async method receives a `SystemPromptContext` with access to the session
311/// filesystem, allowing capabilities to generate dynamic content (e.g., reading
312/// AGENTS.md or scanning for skills).
313///
314/// The default implementation wraps the static `system_prompt_addition()` text
315/// in `<capability id="...">` XML tags. Capabilities that need dynamic content
316/// override `system_prompt_contribution()` directly.
317///
318/// # Example
319///
320/// ```ignore
321/// use everruns_core::capabilities::Capability;
322///
323/// struct CurrentTimeCapability;
324///
325/// impl Capability for CurrentTimeCapability {
326///     fn id(&self) -> &str {
327///         "current_time"
328///     }
329///
330///     fn name(&self) -> &str {
331///         "Current Time"
332///     }
333///
334///     fn description(&self) -> &str {
335///         "Provides tools to get the current date and time."
336///     }
337///
338///     fn tools(&self) -> Vec<Box<dyn Tool>> {
339///         vec![Box::new(GetCurrentTimeTool)]
340///     }
341/// }
342/// ```
343#[async_trait]
344pub trait Capability: Send + Sync {
345    /// Returns the unique capability identifier as a string
346    fn id(&self) -> &str;
347
348    /// Returns the display name
349    fn name(&self) -> &str;
350
351    /// Returns a description of what this capability provides
352    fn description(&self) -> &str;
353
354    /// Returns the current status of this capability
355    fn status(&self) -> CapabilityStatus {
356        CapabilityStatus::Available
357    }
358
359    /// Returns the icon name for UI rendering (optional)
360    fn icon(&self) -> Option<&str> {
361        None
362    }
363
364    /// Returns the category for grouping in UI (optional)
365    fn category(&self) -> Option<&str> {
366        None
367    }
368
369    /// Returns static text to prepend to the agent's system prompt (optional).
370    ///
371    /// This is the simple sync path for capabilities with static prompts.
372    /// For dynamic content that requires filesystem access, override
373    /// `system_prompt_contribution()` instead.
374    ///
375    /// **Contract: no duplication with tool definitions.** System prompt
376    /// additions must NOT repeat information already present in tool names,
377    /// descriptions, or parameter schemas. Only include content that cannot
378    /// be inferred from tool definitions alone:
379    ///
380    /// - High-level semantics (when to use which tool, behavioral guidance)
381    /// - Constraints the model cannot discover from schemas (row limits,
382    ///   naming rules, workspace root paths, scheduling limits)
383    /// - Data layout (filesystem paths for state files)
384    /// - Cross-tool relationships or ordering not evident from descriptions
385    ///
386    /// If every piece of information in the prompt is already covered by the
387    /// tool definitions, return `None` instead.
388    fn system_prompt_addition(&self) -> Option<&str> {
389        None
390    }
391
392    /// Returns the system prompt contribution for this capability, with access
393    /// to session context (filesystem, etc.).
394    ///
395    /// This is the primary method for contributing to the system prompt.
396    /// The returned string is included as-is in the final prompt (the capability
397    /// is responsible for its own XML wrapping).
398    ///
399    /// The default implementation wraps `system_prompt_addition()` in
400    /// `<capability id="...">` XML tags. Capabilities with dynamic content
401    /// (e.g., `agent_instructions`, `skills`) override this to read from the
402    /// session filesystem.
403    async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
404        self.system_prompt_addition().map(|addition| {
405            format!(
406                "<capability id=\"{}\">\n{}\n</capability>",
407                self.id(),
408                addition
409            )
410        })
411    }
412
413    /// Returns a preview of the system prompt addition for UI display.
414    ///
415    /// For most capabilities this is identical to `system_prompt_addition()`.
416    /// Capabilities with dynamic content (e.g. `agent_instructions` which reads
417    /// AGENTS.md at runtime) override this to return a representative preview.
418    fn system_prompt_preview(&self) -> Option<String> {
419        self.system_prompt_addition().map(|s| s.to_string())
420    }
421
422    /// Returns tool implementations provided by this capability
423    fn tools(&self) -> Vec<Box<dyn Tool>> {
424        vec![]
425    }
426
427    /// Returns tool implementations configured by per-capability config.
428    ///
429    /// Called during capability collection with the per-agent config for this
430    /// capability (from `AgentCapabilityConfig.config`). Capabilities that adapt
431    /// their tools based on config override this method.
432    ///
433    /// Default delegates to `tools()`.
434    fn tools_with_config(&self, _config: &serde_json::Value) -> Vec<Box<dyn Tool>> {
435        self.tools()
436    }
437
438    /// Returns system prompt contribution adapted to per-capability config.
439    ///
440    /// Called during capability collection. Capabilities whose system prompt
441    /// content depends on config override this method.
442    ///
443    /// Default delegates to `system_prompt_contribution(ctx)`.
444    async fn system_prompt_contribution_with_config(
445        &self,
446        ctx: &SystemPromptContext,
447        _config: &serde_json::Value,
448    ) -> Option<String> {
449        self.system_prompt_contribution(ctx).await
450    }
451
452    /// Returns tool definitions for the agent config
453    /// By default, converts tools() to definitions
454    fn tool_definitions(&self) -> Vec<ToolDefinition> {
455        self.tools().iter().map(|t| t.to_definition()).collect()
456    }
457
458    /// Returns mount points to populate in the session filesystem
459    ///
460    /// Mount points allow capabilities to provide files and directories
461    /// that are automatically created when a session starts. This is useful
462    /// for providing sample data, documentation, or configuration files.
463    ///
464    /// By default, returns an empty vector (no mounts).
465    fn mounts(&self) -> Vec<MountPoint> {
466        vec![]
467    }
468
469    /// Returns capability IDs that this capability depends on.
470    ///
471    /// Dependencies are automatically resolved at runtime when applying
472    /// capabilities. If capability A depends on capability B, then B's
473    /// contributions (tools, system prompt, mounts) will be included
474    /// when A is selected, even if B is not explicitly selected.
475    ///
476    /// By default, returns an empty vector (no dependencies).
477    fn dependencies(&self) -> Vec<&'static str> {
478        vec![]
479    }
480
481    /// Returns UI feature strings that this capability contributes to.
482    ///
483    /// Features are open-ended strings indicating what user-facing functionality
484    /// this capability enables. Multiple capabilities can contribute the same
485    /// feature (e.g., both `session_schedule` and a future `signals` capability
486    /// might contribute `"schedules"`).
487    ///
488    /// The UI uses the aggregated set of features from all active capabilities
489    /// to decide which tabs/sections to render.
490    ///
491    /// Known features: `"file_system"`, `"schedules"`, `"secrets"`,
492    /// `"key_value"`, `"sql_database"`, `"leased_resources"`.
493    ///
494    /// By default, returns an empty vector (no features).
495    fn features(&self) -> Vec<&'static str> {
496        vec![]
497    }
498
499    /// Returns the JSON Schema for this capability's per-agent config.
500    ///
501    /// The schema is exposed through `CapabilityInfo` so clients can render a
502    /// generic settings editor for capabilities without hard-coding capability
503    /// IDs. Capabilities without configurable settings return `None`.
504    fn config_schema(&self) -> Option<serde_json::Value> {
505        None
506    }
507
508    /// Returns UI hints for rendering `config_schema`.
509    ///
510    /// This follows the react-jsonschema-form `uiSchema` shape. The server owns
511    /// durable config semantics; clients own the generic component implementation.
512    fn config_ui_schema(&self) -> Option<serde_json::Value> {
513        None
514    }
515
516    /// Validates per-capability config before it is persisted.
517    ///
518    /// Default accepts any config for backward compatibility. Capabilities with
519    /// a `config_schema()` should reject invalid values here so HTTP, CLI, and
520    /// MCP write paths share the same server-side guardrail.
521    fn validate_config(&self, _config: &serde_json::Value) -> Result<(), String> {
522        Ok(())
523    }
524
525    /// Returns remote MCP servers contributed by this capability.
526    ///
527    /// These are merged into harness/agent/session scoped MCP config at runtime.
528    /// Explicit scoped MCP config overrides capability-contributed defaults by
529    /// logical server name.
530    fn mcp_servers(&self) -> ScopedMcpServers {
531        ScopedMcpServers::default()
532    }
533
534    /// Returns config-aware remote MCP server contributions.
535    fn mcp_servers_with_config(&self, _config: &serde_json::Value) -> ScopedMcpServers {
536        self.mcp_servers()
537    }
538
539    /// Returns a message filter provider if this capability modifies message retrieval.
540    ///
541    /// Capabilities can contribute filters that modify how messages are loaded
542    /// from the database. This enables features like:
543    /// - Time-based filtering (recent messages only)
544    /// - Event type filtering
545    /// - Tool result filtering by tool name
546    /// - Ephemeral message injection (summaries, reminders)
547    ///
548    /// Filters are applied in capability priority order (by `MessageFilterProvider::priority()`).
549    ///
550    /// By default, returns None (no message filtering).
551    fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
552        None
553    }
554
555    /// Returns a provider that can build a prompt-facing model view from
556    /// lossless stored messages before provider serialization.
557    ///
558    /// This is for capability-owned context transformations such as compaction
559    /// cost-control masking. Storage messages remain unchanged.
560    ///
561    /// By default, returns None (no model-view transformation).
562    fn model_view_provider(&self) -> Option<Arc<dyn ModelViewProvider>> {
563        None
564    }
565
566    /// Returns post-tool execution hooks provided by this capability.
567    ///
568    /// These hooks run after each individual tool completes execution.
569    /// They can persist output, inject metadata, or transform results.
570    /// Capability-contributed hooks run before infrastructure (final) hooks.
571    ///
572    /// By default, returns an empty vector (no hooks).
573    fn post_tool_exec_hooks(&self) -> Vec<Arc<dyn crate::atoms::PostToolExecHook>> {
574        vec![]
575    }
576
577    /// Returns tool definition hooks provided by this capability.
578    ///
579    /// These hooks run after the runtime agent has merged and deduplicated its
580    /// final tool list, before the tool schemas are sent to the LLM. They let
581    /// capabilities apply cross-cutting schema changes to all active tools,
582    /// including tools contributed by other capabilities, MCP, or clients.
583    ///
584    /// By default, returns an empty vector (no tool definition transforms).
585    fn tool_definition_hooks(&self) -> Vec<Arc<dyn ToolDefinitionHook>> {
586        vec![]
587    }
588
589    /// Returns tool call hooks provided by this capability.
590    ///
591    /// These hooks run after the model has produced a tool call. They can read
592    /// model-authored metadata for UI display and transform the tool call used
593    /// for actual execution.
594    ///
595    /// By default, returns an empty vector (no tool call handling).
596    fn tool_call_hooks(&self) -> Vec<Arc<dyn ToolCallHook>> {
597        vec![]
598    }
599
600    /// Returns user-defined hook specifications contributed by this capability.
601    ///
602    /// User hooks are JSON-serializable specs (see
603    /// `crate::user_hook_types::UserHookSpec` and `specs/user-hooks.md`) that
604    /// the `HookAdapterBuilder` validates and turns into per-event
605    /// `Arc<dyn …Hook>` adapters during capability collection. Capabilities
606    /// that ship reusable hook bundles (formatters, security guards, audit
607    /// commands) override this; the user-facing `user_hooks` capability also
608    /// uses this hook to surface user-config-authored entries.
609    ///
610    /// Contributors return *data only* — the executor is constructed
611    /// centrally by the core so global timeout/output/sandbox limits cannot
612    /// be bypassed.
613    ///
614    /// By default, returns an empty vector (no contributed hooks).
615    fn user_hooks(&self) -> Vec<crate::user_hook_types::UserHookSpec> {
616        vec![]
617    }
618
619    /// Returns user-defined hook specifications adapted to per-capability
620    /// config.
621    ///
622    /// Default delegates to `user_hooks()`. The `user_hooks` capability
623    /// overrides this to parse hook entries out of its config.
624    fn user_hooks_with_config(
625        &self,
626        _config: &serde_json::Value,
627    ) -> Vec<crate::user_hook_types::UserHookSpec> {
628        self.user_hooks()
629    }
630
631    /// Returns the risk level of this capability.
632    ///
633    /// TM-AGENT-005: High-risk capabilities (code execution, network access)
634    /// require admin approval when assigned to agents/harnesses. Capabilities
635    /// that combine execution + network access enable data exfiltration.
636    ///
637    /// By default, returns `RiskLevel::Low`.
638    fn risk_level(&self) -> RiskLevel {
639        RiskLevel::Low
640    }
641
642    /// Returns system commands this capability provides.
643    ///
644    /// System commands are user-invocable /slash commands that execute directly
645    /// without involving the LLM. They are surfaced in the UI command palette
646    /// alongside invocable skills.
647    ///
648    /// By default, returns an empty vector (no commands).
649    fn commands(&self) -> Vec<CommandDescriptor> {
650        vec![]
651    }
652
653    /// Execute a system command declared by [`Self::commands`].
654    ///
655    /// Capabilities that declare commands MUST override this. The default
656    /// implementation returns an error so that misconfigurations surface at
657    /// invocation time rather than silently succeeding. Capabilities should
658    /// match on `request.name`, validate `request.arguments`, and use the
659    /// references they captured at construction time to mutate any external
660    /// state (provider store, file system, etc.).
661    ///
662    /// The server's `/btw` flow does not route through this method — it has
663    /// its own bespoke executor because it needs to construct an out-of-band
664    /// LLM call with the session's prompt context. This hook is for
665    /// commands that can execute purely from the capability's own state.
666    async fn execute_command(
667        &self,
668        request: &ExecuteCommandRequest,
669        _ctx: &CommandExecutionContext,
670    ) -> crate::error::Result<CommandResult> {
671        Err(crate::error::AgentLoopError::config(format!(
672            "capability {} declared command /{} but does not implement execute_command",
673            self.id(),
674            request.name,
675        )))
676    }
677
678    /// Returns agent blueprints contributed by this capability.
679    ///
680    /// Blueprints are pre-built agent definitions with private tools, baked-in prompts,
681    /// and fixed/default models. They are spawned via `spawn_subagent(blueprint: "<id>")`.
682    /// Blueprint tools never appear in the host agent's tool list.
683    ///
684    /// By default, returns an empty vector (no blueprints).
685    fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
686        vec![]
687    }
688
689    /// Returns skills contributed by this capability in code.
690    ///
691    /// Contributions are normalized during capability collection into read-only
692    /// mount points at `/.agents/skills/{name}/` so the built-in `skills`
693    /// capability discovers them alongside user-uploaded and registry-based
694    /// skills. This keeps discovery, prompt listing, and activation in one
695    /// place rather than adding a parallel skill pipeline.
696    ///
697    /// By default, returns an empty vector (no contributed skills).
698    fn contribute_skills(&self) -> Vec<SkillContribution> {
699        vec![]
700    }
701
702    /// Returns streaming output guardrails contributed by this capability.
703    ///
704    /// Each provider is armed once per assistant message stream with the
705    /// fully assembled system prompt and per-capability config; the returned
706    /// per-stream `OutputGuardrailRun` is invoked after every batched delta
707    /// in the streaming hot path. Returning `Block` aborts the stream and
708    /// the client is told to replace the accumulated text with a canned
709    /// message. See [`crate::output_guardrail`].
710    ///
711    /// Default: no guardrails.
712    fn output_guardrails(&self) -> Vec<Arc<dyn crate::output_guardrail::OutputGuardrail>> {
713        vec![]
714    }
715}
716
717pub trait ToolDefinitionHook: Send + Sync {
718    fn transform(&self, tools: Vec<ToolDefinition>) -> Vec<ToolDefinition>;
719}
720
721pub trait ToolCallHook: Send + Sync {
722    fn narration(
723        &self,
724        _tool_def: Option<&ToolDefinition>,
725        _tool_call: &ToolCall,
726        _phase: crate::tool_narration::ToolNarrationPhase,
727        _locale: Option<&str>,
728    ) -> Option<String> {
729        None
730    }
731
732    fn transform_for_execution(&self, tool_call: ToolCall) -> ToolCall {
733        tool_call
734    }
735}
736
737/// Risk classification for capabilities (TM-AGENT-005).
738///
739/// Used to enforce approval requirements when assigning capabilities.
740#[derive(
741    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
742)]
743#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
744#[cfg_attr(feature = "openapi", schema(example = "low"))]
745#[serde(rename_all = "lowercase")]
746pub enum RiskLevel {
747    /// No special approval needed
748    Low,
749    /// Logged but allowed for org members
750    Medium,
751    /// Requires org admin role to assign
752    High,
753}
754
755// ============================================================================
756// Agent Blueprints
757// ============================================================================
758
759/// Model selection strategy for agent blueprints.
760#[derive(Debug, Clone, Serialize, Deserialize)]
761#[serde(rename_all = "snake_case")]
762pub enum BlueprintModel {
763    /// Always use this model. Host cannot override.
764    Fixed(String),
765    /// Use this model unless host provides override via config.
766    Default(String),
767    /// Use whatever model the host agent uses.
768    Inherit,
769}
770
771/// Pre-built agent definition with private tools, baked-in prompt, and model selection.
772///
773/// Contributed by capabilities via `agent_blueprints()`. Spawned via
774/// `spawn_subagent(blueprint: "<id>")`. Blueprint tools never appear in the
775/// host agent's tool list — they exist only inside the spawned child session.
776pub struct AgentBlueprint {
777    /// Unique identifier (e.g. `"github_scout"`)
778    pub id: &'static str,
779    /// Human-readable display name
780    pub name: &'static str,
781    /// When to use this blueprint (LLM reads this for delegation decisions)
782    pub description: &'static str,
783    /// Model selection strategy
784    pub model: BlueprintModel,
785    /// Baked-in system prompt for the child agent
786    pub system_prompt: &'static str,
787    /// Private tools — only available inside the blueprint's session
788    pub tools: Vec<Box<dyn Tool>>,
789    /// Iteration limit (default: 20)
790    pub max_turns: Option<usize>,
791    /// JSON Schema for allowed host-provided config. `None` = no config accepted.
792    pub config_schema: Option<serde_json::Value>,
793}
794
795impl AgentBlueprint {
796    /// Convert blueprint tools to tool definitions (for RuntimeAgent building).
797    pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
798        self.tools.iter().map(|t| t.to_definition()).collect()
799    }
800}
801
802impl std::fmt::Debug for AgentBlueprint {
803    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804        f.debug_struct("AgentBlueprint")
805            .field("id", &self.id)
806            .field("name", &self.name)
807            .field("model", &self.model)
808            .field("tool_count", &self.tools.len())
809            .field("max_turns", &self.max_turns)
810            .finish()
811    }
812}
813
814// ============================================================================
815// Capability Registry
816// ============================================================================
817
818/// Registry that holds all available capability implementations.
819///
820/// The registry provides access to capabilities by ID and allows
821/// applying multiple capabilities to build a RuntimeAgent.
822///
823/// # Example
824///
825/// ```
826/// use everruns_core::capabilities::CapabilityRegistry;
827///
828/// let registry = CapabilityRegistry::with_builtins();
829///
830/// // Get a capability by ID
831/// if let Some(cap) = registry.get("current_time") {
832///     println!("Capability: {}", cap.name());
833/// }
834///
835/// // List all available capabilities
836/// for cap in registry.list() {
837///     println!("{}: {}", cap.id(), cap.name());
838/// }
839/// ```
840#[derive(Clone)]
841pub struct CapabilityRegistry {
842    capabilities: HashMap<String, Arc<dyn Capability>>,
843}
844
845impl CapabilityRegistry {
846    /// Create a new empty registry
847    pub fn new() -> Self {
848        Self {
849            capabilities: HashMap::new(),
850        }
851    }
852
853    /// Create a registry with all built-in capabilities registered
854    ///
855    /// Uses `DeploymentGrade::from_env()` to determine which capabilities to include.
856    /// For explicit control, use `with_builtins_for_grade()`.
857    pub fn with_builtins() -> Self {
858        Self::with_builtins_for_grade(DeploymentGrade::from_env())
859    }
860
861    /// Create a registry with built-in capabilities for a specific deployment grade
862    ///
863    /// Experimental capabilities are included via integration plugins in dev environments.
864    /// Non-experimental integration plugins (like Daytona) are included in all environments.
865    pub fn with_builtins_for_grade(grade: DeploymentGrade) -> Self {
866        let mut registry = Self::new();
867
868        // Core capabilities (all environments)
869        registry.register(AgentInstructionsCapability);
870        registry.register(HumanIntentCapability);
871        registry.register(NoopCapability);
872        registry.register(CurrentTimeCapability);
873        registry.register(ResearchCapability);
874        registry.register(PlatformManagementCapability);
875        registry.register(FileSystemCapability);
876        registry.register(WorkspaceVolumesCapability);
877        registry.register(SessionStorageCapability);
878        registry.register(SessionCapability);
879        registry.register(SessionSqlDatabaseCapability);
880        registry.register(TestMathCapability);
881        registry.register(TestWeatherCapability);
882        registry.register(StatelessTodoListCapability);
883        registry.register(WebFetchCapability::from_env());
884        registry.register(VirtualBashCapability);
885        registry.register(BackgroundExecutionCapability);
886        registry.register(SessionScheduleCapability);
887        registry.register(BtwCapability);
888        registry.register(InfinityContextCapability);
889        registry.register(budgeting::BudgetingCapability);
890        registry.register(SelfBudgetCapability);
891        registry.register(ParallelCapability);
892        registry.register(CompactionCapability);
893        registry.register(MemoryCapability);
894
895        // OpenAI tool_search (deferred tool loading, all environments)
896        registry.register(OpenAiToolSearchCapability::new());
897        registry.register(PromptCachingCapability::new());
898
899        // Skills (filesystem-based discovery + activation, all environments)
900        registry.register(SkillsCapability);
901
902        // Subagents (spawn child agent sessions, all environments)
903        registry.register(SubagentCapability);
904
905        // Outbound agent delegation — experimental (dev-only by default).
906        // Risk: exfil, SSRF-adjacent reach, cost/recursion fan-out.
907        // Gated by FEATURE_AGENT_DELEGATION; auto-enabled in dev, off in prod.
908        if crate::FeatureFlags::from_env(&grade).agent_delegation {
909            registry.register(AgentHandoffCapability);
910            registry.register(A2aAgentDelegationCapability);
911        }
912
913        // System commands (/clear, /status, /compact, /model)
914        registry.register(SystemCommandsCapability);
915
916        // Tool output persistence (EVE-222: persist exec output to VFS)
917        registry.register(tool_output_persistence::ToolOutputPersistenceCapability);
918
919        // User hooks (see specs/user-hooks.md): user-authored shell commands
920        // at lifecycle/tool events. Risk: High.
921        registry.register(user_hooks::UserHooksCapability);
922
923        // Loop detection (EVE-227: detect repeated identical tool calls)
924        registry.register(LoopDetectionCapability);
925
926        // Prompt canary guardrail: replace assistant output if it leaks the
927        // first sentence of the system prompt. Streaming-output guardrail.
928        registry.register(PromptCanaryGuardrailCapability);
929
930        // OpenUI/A2UI prompt helpers are product features, not required by embedders.
931        #[cfg(feature = "ui-capabilities")]
932        {
933            registry.register(OpenUiCapability);
934            registry.register(A2UiCapability);
935        }
936
937        // Demo capability with mount points (all environments)
938        registry.register(SampleDataCapability);
939
940        // Data knowledge scaffold (all environments)
941        registry.register(DataKnowledgeCapability);
942
943        // Knowledge bases (curated org knowledge — see specs/knowledge-bases.md)
944        registry.register(KnowledgeBaseCapability);
945
946        // Fake demo capabilities (all environments)
947        registry.register(FakeWarehouseCapability);
948        registry.register(FakeAwsCapability);
949        registry.register(FakeCrmCapability);
950        registry.register(FakeFinancialCapability);
951
952        // External integration plugins (registered via inventory::submit! in integration crates)
953        let internal_flags = crate::InternalFeatureFlags::from_env();
954        if internal_flags.session_sandbox {
955            registry.register(SessionSandboxCapability);
956        }
957        for plugin in inventory::iter::<IntegrationPlugin>() {
958            if (!plugin.experimental_only || grade.experimental_features_enabled())
959                && plugin
960                    .feature_flag
961                    .is_none_or(|f| internal_flags.is_enabled(f))
962            {
963                registry.register_boxed((plugin.factory)());
964            }
965        }
966
967        registry
968    }
969
970    /// Register a capability
971    pub fn register(&mut self, capability: impl Capability + 'static) {
972        self.capabilities
973            .insert(capability.id().to_string(), Arc::new(capability));
974    }
975
976    /// Register a boxed capability
977    pub fn register_boxed(&mut self, capability: Box<dyn Capability>) {
978        self.capabilities
979            .insert(capability.id().to_string(), Arc::from(capability));
980    }
981
982    /// Register an Arc-wrapped capability
983    pub fn register_arc(&mut self, capability: Arc<dyn Capability>) {
984        self.capabilities
985            .insert(capability.id().to_string(), capability);
986    }
987
988    /// Get a capability by ID
989    pub fn get(&self, id: &str) -> Option<&Arc<dyn Capability>> {
990        self.capabilities.get(id)
991    }
992
993    /// Remove a capability from the registry.
994    pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Capability>> {
995        self.capabilities.remove(id)
996    }
997
998    /// Check if a capability is registered
999    pub fn has(&self, id: &str) -> bool {
1000        self.capabilities.contains_key(id)
1001    }
1002
1003    /// Get all registered capabilities
1004    pub fn list(&self) -> Vec<&Arc<dyn Capability>> {
1005        self.capabilities.values().collect()
1006    }
1007
1008    /// Get the number of registered capabilities
1009    pub fn len(&self) -> usize {
1010        self.capabilities.len()
1011    }
1012
1013    /// Check if the registry is empty
1014    pub fn is_empty(&self) -> bool {
1015        self.capabilities.is_empty()
1016    }
1017
1018    /// Create a builder for fluent capability registration
1019    pub fn builder() -> CapabilityRegistryBuilder {
1020        CapabilityRegistryBuilder::new()
1021    }
1022
1023    /// Find a blueprint by ID across all registered capabilities.
1024    ///
1025    /// Returns a fresh `AgentBlueprint` (with new tool instances) each time.
1026    pub fn blueprint(&self, id: &str) -> Option<AgentBlueprint> {
1027        for cap in self.capabilities.values() {
1028            for bp in cap.agent_blueprints() {
1029                if bp.id == id {
1030                    return Some(bp);
1031                }
1032            }
1033        }
1034        None
1035    }
1036
1037    /// Find a blueprint and the capability that registered it.
1038    ///
1039    /// Returns `(capability_id, blueprint)` with fresh tool instances.
1040    pub fn blueprint_with_capability(&self, id: &str) -> Option<(String, AgentBlueprint)> {
1041        for (capability_id, cap) in &self.capabilities {
1042            for bp in cap.agent_blueprints() {
1043                if bp.id == id {
1044                    return Some((capability_id.clone(), bp));
1045                }
1046            }
1047        }
1048        None
1049    }
1050
1051    /// Collect all blueprints from all registered capabilities.
1052    pub fn all_blueprints(&self) -> Vec<AgentBlueprint> {
1053        self.capabilities
1054            .values()
1055            .flat_map(|cap| cap.agent_blueprints())
1056            .collect()
1057    }
1058}
1059
1060impl Default for CapabilityRegistry {
1061    fn default() -> Self {
1062        Self::with_builtins()
1063    }
1064}
1065
1066impl std::fmt::Debug for CapabilityRegistry {
1067    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068        let ids: Vec<_> = self.capabilities.keys().collect();
1069        f.debug_struct("CapabilityRegistry")
1070            .field("capabilities", &ids)
1071            .finish()
1072    }
1073}
1074
1075/// Builder for creating a CapabilityRegistry with a fluent API
1076pub struct CapabilityRegistryBuilder {
1077    registry: CapabilityRegistry,
1078}
1079
1080impl CapabilityRegistryBuilder {
1081    /// Create a new builder with an empty registry
1082    pub fn new() -> Self {
1083        Self {
1084            registry: CapabilityRegistry::new(),
1085        }
1086    }
1087
1088    /// Create a new builder with built-in capabilities
1089    pub fn with_builtins() -> Self {
1090        Self {
1091            registry: CapabilityRegistry::with_builtins(),
1092        }
1093    }
1094
1095    /// Add a capability
1096    pub fn capability(mut self, capability: impl Capability + 'static) -> Self {
1097        self.registry.register(capability);
1098        self
1099    }
1100
1101    /// Build the registry
1102    pub fn build(self) -> CapabilityRegistry {
1103        self.registry
1104    }
1105}
1106
1107impl Default for CapabilityRegistryBuilder {
1108    fn default() -> Self {
1109        Self::new()
1110    }
1111}
1112
1113// ============================================================================
1114// Collect Capabilities Helper
1115// ============================================================================
1116
1117/// Context available to capability-owned model-view transforms.
1118pub struct ModelViewContext<'a> {
1119    pub session_id: SessionId,
1120    pub prior_usage: Option<&'a TokenUsage>,
1121}
1122
1123/// Provider-side hook for building prompt-facing model views.
1124///
1125/// Providers receive the output of earlier providers and return the messages
1126/// that should be sent into provider serialization. Lower priority providers
1127/// run earlier.
1128pub trait ModelViewProvider: Send + Sync {
1129    fn apply_model_view(
1130        &self,
1131        messages: Vec<Message>,
1132        config: &serde_json::Value,
1133        context: &ModelViewContext<'_>,
1134    ) -> Vec<Message>;
1135
1136    fn priority(&self) -> i32 {
1137        0
1138    }
1139}
1140
1141/// Collected data from capabilities before applying to config.
1142///
1143/// This intermediate struct allows sharing the capability collection logic
1144/// between `apply_capabilities` and `apply_capabilities_to_builder`.
1145pub struct CollectedCapabilities {
1146    /// System prompt additions (in order)
1147    pub system_prompt_parts: Vec<String>,
1148    /// Source attribution for each system prompt addition.
1149    pub system_prompt_attributions: Vec<SystemPromptAttribution>,
1150    /// Tool implementations for the registry
1151    pub tools: Vec<Box<dyn Tool>>,
1152    /// Tool definitions for config
1153    pub tool_definitions: Vec<ToolDefinition>,
1154    /// Mount points from capabilities
1155    pub mounts: Vec<MountPoint>,
1156    /// Message filter providers with their configs (in priority order)
1157    pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1158    /// IDs of capabilities that were collected
1159    pub applied_ids: Vec<String>,
1160    /// Tool search configuration (set when openai_tool_search capability is present)
1161    pub tool_search: Option<crate::llm_driver_registry::ToolSearchConfig>,
1162    /// Prompt caching configuration (set when prompt_caching capability is present)
1163    pub prompt_cache: Option<crate::llm_driver_registry::PromptCacheConfig>,
1164    /// Hooks that transform the final runtime tool definition list.
1165    pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
1166    /// Hooks that inspect or transform model-produced tool calls.
1167    pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
1168    /// Scoped remote MCP servers contributed by capabilities.
1169    pub mcp_servers: ScopedMcpServers,
1170    // NOTE: output guardrails are intentionally NOT collected here. They are
1171    // re-derived per turn in `ReasonAtom` directly from the resolved capability
1172    // configs + registry, because they need the assembled system prompt at
1173    // arming time (which only exists once the runtime agent is built). Storing
1174    // them here would duplicate that work for callers that don't run a stream.
1175}
1176
1177#[derive(Debug, Clone, PartialEq, Eq)]
1178pub struct SystemPromptAttribution {
1179    pub capability_id: String,
1180    pub content: String,
1181}
1182
1183impl CollectedCapabilities {
1184    /// Returns the combined system prompt prefix from all capabilities.
1185    /// Returns None if no capabilities contributed system prompt additions.
1186    pub fn system_prompt_prefix(&self) -> Option<String> {
1187        if self.system_prompt_parts.is_empty() {
1188            None
1189        } else {
1190            Some(self.system_prompt_parts.join("\n\n"))
1191        }
1192    }
1193
1194    /// Apply all collected message filter providers to a query.
1195    ///
1196    /// Providers are applied in priority order (lower priority first).
1197    pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1198        // Providers are already sorted by priority during collection
1199        for (provider, config) in &self.message_filter_providers {
1200            provider.apply_filters(query, config);
1201        }
1202    }
1203
1204    /// Apply post-load transforms from all message filter providers.
1205    /// Called after messages are loaded, filtered, and injected.
1206    pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1207        for (provider, config) in &self.message_filter_providers {
1208            provider.post_load(messages, config);
1209        }
1210    }
1211
1212    /// Check if any capabilities contribute message filters.
1213    pub fn has_message_filters(&self) -> bool {
1214        !self.message_filter_providers.is_empty()
1215    }
1216}
1217
1218/// Lightweight result containing only message filter providers.
1219///
1220/// Used when callers only need message filtering (e.g., message loading in
1221/// ReasonAtom) without paying the cost of system prompt contribution or tool
1222/// collection. This avoids unnecessary filesystem reads (AGENTS.md) and tool
1223/// instantiation on the message-filter-only path.
1224pub struct CollectedMessageFilters {
1225    /// Message filter providers with their configs (in priority order)
1226    pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
1227}
1228
1229/// Lightweight result containing only model-view providers.
1230pub struct CollectedModelViewProviders {
1231    /// Model-view providers with their configs (in priority order).
1232    pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
1233}
1234
1235// Note: apply_message_filters/apply_post_load_filters mirror the same methods
1236// on CollectedCapabilities. The duplication is intentional — extracting a trait
1237// would add indirection for 3 lines of loop body, and the two structs serve
1238// different purposes (lightweight vs full collection).
1239
1240impl CollectedMessageFilters {
1241    /// Apply all collected message filter providers to a query.
1242    pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
1243        for (provider, config) in &self.message_filter_providers {
1244            provider.apply_filters(query, config);
1245        }
1246    }
1247
1248    /// Apply post-load transforms from all message filter providers.
1249    pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
1250        for (provider, config) in &self.message_filter_providers {
1251            provider.post_load(messages, config);
1252        }
1253    }
1254}
1255
1256impl CollectedModelViewProviders {
1257    /// Apply all collected model-view providers in priority order.
1258    pub fn apply_model_view(
1259        &self,
1260        mut messages: Vec<Message>,
1261        context: &ModelViewContext<'_>,
1262    ) -> Vec<Message> {
1263        for (provider, config) in &self.model_view_providers {
1264            messages = provider.apply_model_view(messages, config, context);
1265        }
1266        messages
1267    }
1268}
1269
1270/// Collect only message filter providers from capabilities, skipping system
1271/// prompt contributions, tools, mounts, and other expensive work.
1272///
1273/// This is a fast path for callers that only need message filtering (e.g.,
1274/// the message-loading step in ReasonAtom before RuntimeAgent is built).
1275pub fn collect_message_filters_only(
1276    capability_configs: &[AgentCapabilityConfig],
1277    registry: &CapabilityRegistry,
1278) -> CollectedMessageFilters {
1279    let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1280        Vec::new();
1281
1282    for cap_config in capability_configs {
1283        let cap_id = cap_config.capability_ref.as_str();
1284        if let Some(capability) = registry.get(cap_id) {
1285            if capability.status() != CapabilityStatus::Available {
1286                continue;
1287            }
1288            if let Some(provider) = capability.message_filter_provider() {
1289                message_filter_providers.push((provider, cap_config.config.clone()));
1290            }
1291        }
1292    }
1293
1294    message_filter_providers.sort_by_key(|(p, _)| p.priority());
1295
1296    CollectedMessageFilters {
1297        message_filter_providers,
1298    }
1299}
1300
1301/// Collect only model-view providers from capabilities.
1302pub fn collect_model_view_providers(
1303    capability_configs: &[AgentCapabilityConfig],
1304    registry: &CapabilityRegistry,
1305) -> CollectedModelViewProviders {
1306    let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
1307
1308    for cap_config in capability_configs {
1309        let cap_id = cap_config.capability_ref.as_str();
1310        if let Some(capability) = registry.get(cap_id) {
1311            if capability.status() != CapabilityStatus::Available {
1312                continue;
1313            }
1314            if let Some(provider) = capability.model_view_provider() {
1315                model_view_providers.push((provider, cap_config.config.clone()));
1316            }
1317        }
1318    }
1319
1320    model_view_providers.sort_by_key(|(p, _)| p.priority());
1321
1322    CollectedModelViewProviders {
1323        model_view_providers,
1324    }
1325}
1326
1327pub fn collect_capability_mcp_servers(
1328    capability_configs: &[AgentCapabilityConfig],
1329    registry: &CapabilityRegistry,
1330) -> ScopedMcpServers {
1331    let mut servers = ScopedMcpServers::default();
1332
1333    for cap_config in capability_configs {
1334        let cap_id = cap_config.capability_ref.as_str();
1335        if is_declarative_capability(cap_id) {
1336            if let Ok(definition) =
1337                serde_json::from_value::<DeclarativeCapabilityDefinition>(cap_config.config.clone())
1338            {
1339                if definition.status != CapabilityStatus::Available {
1340                    continue;
1341                }
1342                if let Some(contributed) = definition.mcp_servers {
1343                    servers = merge_scoped_mcp_servers(&servers, &contributed);
1344                }
1345            }
1346            continue;
1347        }
1348        if let Some(capability) = registry.get(cap_id) {
1349            if capability.status() != CapabilityStatus::Available {
1350                continue;
1351            }
1352            servers = merge_scoped_mcp_servers(
1353                &servers,
1354                &capability.mcp_servers_with_config(&cap_config.config),
1355            );
1356        }
1357    }
1358
1359    servers
1360}
1361
1362// ============================================================================
1363// Dependency Resolution
1364// ============================================================================
1365
1366/// Maximum number of capabilities after dependency resolution.
1367/// This prevents runaway dependency chains and resource exhaustion.
1368pub const MAX_RESOLVED_CAPABILITIES: usize = 100;
1369
1370/// Error type for dependency resolution failures
1371#[derive(Debug, Clone, PartialEq, Eq)]
1372pub enum DependencyError {
1373    /// Circular dependency detected in the capability graph
1374    CircularDependency {
1375        /// The capability where the cycle was detected
1376        capability_id: String,
1377        /// The dependency chain leading to the cycle
1378        chain: Vec<String>,
1379    },
1380    /// Too many capabilities after resolution
1381    TooManyCapabilities {
1382        /// Number of capabilities requested
1383        count: usize,
1384        /// Maximum allowed
1385        max: usize,
1386    },
1387}
1388
1389impl std::fmt::Display for DependencyError {
1390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1391        match self {
1392            DependencyError::CircularDependency {
1393                capability_id,
1394                chain,
1395            } => {
1396                write!(
1397                    f,
1398                    "Circular dependency detected: {} depends on itself via chain: {} -> {}",
1399                    capability_id,
1400                    chain.join(" -> "),
1401                    capability_id
1402                )
1403            }
1404            DependencyError::TooManyCapabilities { count, max } => {
1405                write!(
1406                    f,
1407                    "Too many capabilities after resolution: {} (max: {})",
1408                    count, max
1409                )
1410            }
1411        }
1412    }
1413}
1414
1415impl std::error::Error for DependencyError {}
1416
1417/// Result of resolving capability dependencies
1418#[derive(Debug, Clone)]
1419pub struct ResolvedCapabilities {
1420    /// All capability IDs after resolving dependencies (in topological order)
1421    /// Dependencies come before dependents.
1422    pub resolved_ids: Vec<String>,
1423    /// IDs that were added as dependencies (not in the original selection)
1424    pub added_as_dependencies: Vec<String>,
1425    /// Original user-selected capability IDs
1426    pub user_selected: Vec<String>,
1427}
1428
1429/// Resolve capability dependencies, returning all required capability IDs.
1430///
1431/// This function:
1432/// 1. Takes the user-selected capability IDs
1433/// 2. Recursively collects all dependencies
1434/// 3. Returns them in topological order (dependencies before dependents)
1435/// 4. Detects circular dependencies and returns an error
1436/// 5. Enforces a maximum capability limit
1437///
1438/// # Arguments
1439///
1440/// * `selected_ids` - User-selected capability IDs
1441/// * `registry` - The capability registry to look up dependencies
1442///
1443/// # Returns
1444///
1445/// `Ok(ResolvedCapabilities)` with all required capabilities in order,
1446/// or `Err(DependencyError)` if circular dependencies are detected or
1447/// the limit is exceeded.
1448pub fn resolve_dependencies(
1449    selected_ids: &[String],
1450    registry: &CapabilityRegistry,
1451) -> Result<ResolvedCapabilities, DependencyError> {
1452    use std::collections::HashSet;
1453
1454    let user_selected: HashSet<String> = selected_ids.iter().cloned().collect();
1455    let mut resolved: Vec<String> = Vec::new();
1456    let mut resolved_set: HashSet<String> = HashSet::new();
1457    let mut added_as_dependencies: Vec<String> = Vec::new();
1458
1459    // Process each selected capability and its dependencies using DFS
1460    for cap_id in selected_ids {
1461        resolve_single_capability(
1462            cap_id,
1463            registry,
1464            &mut resolved,
1465            &mut resolved_set,
1466            &mut added_as_dependencies,
1467            &user_selected,
1468            &mut Vec::new(), // visiting chain for cycle detection
1469        )?;
1470    }
1471
1472    // Check max limit
1473    if resolved.len() > MAX_RESOLVED_CAPABILITIES {
1474        return Err(DependencyError::TooManyCapabilities {
1475            count: resolved.len(),
1476            max: MAX_RESOLVED_CAPABILITIES,
1477        });
1478    }
1479
1480    Ok(ResolvedCapabilities {
1481        resolved_ids: resolved,
1482        added_as_dependencies,
1483        user_selected: selected_ids.to_vec(),
1484    })
1485}
1486
1487/// Resolve dependency-expanded capability configs, preserving explicit config on selected IDs.
1488///
1489/// Dependencies are inserted with empty configs. If the same capability is provided more than
1490/// once, the last explicit config wins.
1491pub fn resolve_capability_configs(
1492    selected_configs: &[AgentCapabilityConfig],
1493    registry: &CapabilityRegistry,
1494) -> Result<Vec<AgentCapabilityConfig>, DependencyError> {
1495    let mut selected_ids: Vec<String> = Vec::new();
1496    for config in selected_configs {
1497        if is_declarative_capability(config.capability_id())
1498            && let Ok(definition) =
1499                serde_json::from_value::<DeclarativeCapabilityDefinition>(config.config.clone())
1500        {
1501            selected_ids.extend(definition.dependencies);
1502        }
1503        selected_ids.push(config.capability_id().to_string());
1504    }
1505    let resolved = resolve_dependencies(&selected_ids, registry)?;
1506
1507    let explicit_configs: std::collections::HashMap<String, serde_json::Value> = selected_configs
1508        .iter()
1509        .map(|config| (config.capability_id().to_string(), config.config.clone()))
1510        .collect();
1511
1512    Ok(resolved
1513        .resolved_ids
1514        .into_iter()
1515        .map(|capability_id| {
1516            explicit_configs
1517                .get(&capability_id)
1518                .cloned()
1519                .map(|config| AgentCapabilityConfig::with_config(capability_id.clone(), config))
1520                .unwrap_or_else(|| AgentCapabilityConfig::new(capability_id))
1521        })
1522        .collect())
1523}
1524
1525/// Helper function to resolve a single capability and its dependencies recursively.
1526fn resolve_single_capability(
1527    cap_id: &str,
1528    registry: &CapabilityRegistry,
1529    resolved: &mut Vec<String>,
1530    resolved_set: &mut std::collections::HashSet<String>,
1531    added_as_dependencies: &mut Vec<String>,
1532    user_selected: &std::collections::HashSet<String>,
1533    visiting: &mut Vec<String>,
1534) -> Result<(), DependencyError> {
1535    // Already resolved
1536    if resolved_set.contains(cap_id) {
1537        return Ok(());
1538    }
1539
1540    // Check for circular dependency
1541    if visiting.contains(&cap_id.to_string()) {
1542        return Err(DependencyError::CircularDependency {
1543            capability_id: cap_id.to_string(),
1544            chain: visiting.clone(),
1545        });
1546    }
1547
1548    // Get capability from registry
1549    let capability = match registry.get(cap_id) {
1550        Some(cap) => cap,
1551        None => {
1552            if is_declarative_capability(cap_id) && !resolved_set.contains(cap_id) {
1553                resolved.push(cap_id.to_string());
1554                resolved_set.insert(cap_id.to_string());
1555                if !user_selected.contains(cap_id) {
1556                    added_as_dependencies.push(cap_id.to_string());
1557                }
1558            }
1559            return Ok(());
1560        }
1561    };
1562
1563    // Mark as visiting
1564    visiting.push(cap_id.to_string());
1565
1566    // Resolve dependencies first (depth-first)
1567    for dep_id in capability.dependencies() {
1568        resolve_single_capability(
1569            dep_id,
1570            registry,
1571            resolved,
1572            resolved_set,
1573            added_as_dependencies,
1574            user_selected,
1575            visiting,
1576        )?;
1577    }
1578
1579    // Remove from visiting
1580    visiting.pop();
1581
1582    // Add to resolved
1583    if !resolved_set.contains(cap_id) {
1584        resolved.push(cap_id.to_string());
1585        resolved_set.insert(cap_id.to_string());
1586
1587        // Track if this was added as a dependency (not user-selected)
1588        if !user_selected.contains(cap_id) {
1589            added_as_dependencies.push(cap_id.to_string());
1590        }
1591    }
1592
1593    Ok(())
1594}
1595
1596/// Compute the aggregated set of UI features from a list of capability IDs.
1597///
1598/// Resolves dependencies, collects features from all resolved capabilities,
1599/// and returns deduplicated feature strings.
1600pub fn compute_features(capability_ids: &[String], registry: &CapabilityRegistry) -> Vec<String> {
1601    use std::collections::HashSet;
1602
1603    let resolved_ids = match resolve_dependencies(capability_ids, registry) {
1604        Ok(resolved) => resolved.resolved_ids,
1605        Err(_) => capability_ids.to_vec(),
1606    };
1607
1608    let mut seen = HashSet::new();
1609    let mut features = Vec::new();
1610    for cap_id in &resolved_ids {
1611        if let Some(cap) = registry.get(cap_id) {
1612            for feature in cap.features() {
1613                if seen.insert(feature) {
1614                    features.push(feature.to_string());
1615                }
1616            }
1617        }
1618    }
1619    features
1620}
1621
1622/// Get direct dependencies for a capability ID.
1623/// Returns empty vec if capability not found.
1624pub fn get_dependencies(cap_id: &str, registry: &CapabilityRegistry) -> Vec<String> {
1625    registry
1626        .get(cap_id)
1627        .map(|cap| cap.dependencies().iter().map(|s| s.to_string()).collect())
1628        .unwrap_or_default()
1629}
1630
1631/// Collect contributions from capabilities without applying them.
1632///
1633/// Resolves dependencies first, then calls `system_prompt_contribution()` (async)
1634/// on each capability, enabling dynamic content generation based on session context
1635/// (e.g., reading AGENTS.md, discovering skills).
1636///
1637/// Note: This function does not collect message filter providers since it doesn't
1638/// have access to per-agent capability configs. Use `collect_capabilities_with_configs`
1639/// if you need message filter providers.
1640///
1641/// # Arguments
1642///
1643/// * `capability_ids` - Ordered list of capability IDs to collect
1644/// * `registry` - The capability registry containing implementations
1645/// * `ctx` - Session context for dynamic prompt resolution
1646pub async fn collect_capabilities(
1647    capability_ids: &[String],
1648    registry: &CapabilityRegistry,
1649    ctx: &SystemPromptContext,
1650) -> CollectedCapabilities {
1651    // Resolve dependencies so that transitive capabilities (e.g. session_storage
1652    // via browserless) are included automatically.
1653    let resolved_ids = match resolve_dependencies(capability_ids, registry) {
1654        Ok(resolved) => resolved.resolved_ids,
1655        Err(e) => {
1656            tracing::warn!("Failed to resolve capability dependencies: {}", e);
1657            capability_ids.to_vec()
1658        }
1659    };
1660
1661    // Convert to AgentCapabilityConfig with empty configs
1662    let configs: Vec<AgentCapabilityConfig> = resolved_ids
1663        .iter()
1664        .map(|id| AgentCapabilityConfig {
1665            capability_ref: CapabilityId::new(id),
1666            config: serde_json::Value::Object(serde_json::Map::new()),
1667        })
1668        .collect();
1669
1670    collect_capabilities_with_configs(&configs, registry, ctx).await
1671}
1672
1673/// Collect contributions from capabilities with their per-agent configurations.
1674///
1675/// Calls `system_prompt_contribution()` (async) on each capability, enabling
1676/// dynamic content generation based on session context.
1677///
1678/// # Arguments
1679///
1680/// * `capability_configs` - Ordered list of capability configs (ID + per-agent config)
1681/// * `registry` - The capability registry containing implementations
1682/// * `ctx` - Session context for dynamic prompt resolution
1683pub async fn collect_capabilities_with_configs(
1684    capability_configs: &[AgentCapabilityConfig],
1685    registry: &CapabilityRegistry,
1686    ctx: &SystemPromptContext,
1687) -> CollectedCapabilities {
1688    let mut system_prompt_parts: Vec<String> = Vec::new();
1689    let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
1690    let mut tools: Vec<Box<dyn Tool>> = Vec::new();
1691    let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
1692    let mut mounts: Vec<MountPoint> = Vec::new();
1693    let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
1694        Vec::new();
1695    let mut applied_ids: Vec<String> = Vec::new();
1696    let mut tool_search: Option<crate::llm_driver_registry::ToolSearchConfig> = None;
1697    let mut prompt_cache: Option<crate::llm_driver_registry::PromptCacheConfig> = None;
1698    let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
1699    let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
1700    let mut mcp_servers = ScopedMcpServers::default();
1701
1702    for cap_config in capability_configs {
1703        let cap_id = cap_config.capability_ref.as_str();
1704        if is_declarative_capability(cap_id) {
1705            match serde_json::from_value::<DeclarativeCapabilityDefinition>(
1706                cap_config.config.clone(),
1707            ) {
1708                Ok(definition) => {
1709                    if definition.status != CapabilityStatus::Available {
1710                        continue;
1711                    }
1712
1713                    if let Some(prompt) = definition.system_prompt.as_deref() {
1714                        let contribution =
1715                            format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
1716                        system_prompt_attributions.push(SystemPromptAttribution {
1717                            capability_id: cap_id.to_string(),
1718                            content: contribution.clone(),
1719                        });
1720                        system_prompt_parts.push(contribution);
1721                    }
1722
1723                    mounts.extend(definition.mounts(cap_id));
1724                    if let Some(ref servers) = definition.mcp_servers {
1725                        mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
1726                    }
1727                    for skill in definition.skill_contributions() {
1728                        mounts.push(skill.to_mount(cap_id));
1729                    }
1730
1731                    applied_ids.push(cap_id.to_string());
1732                }
1733                Err(error) => {
1734                    tracing::warn!(
1735                        capability_id = %cap_id,
1736                        error = %error,
1737                        "Skipping invalid declarative capability config"
1738                    );
1739                }
1740            }
1741            continue;
1742        }
1743        if let Some(capability) = registry.get(cap_id) {
1744            // Only collect from available capabilities
1745            if capability.status() != CapabilityStatus::Available {
1746                continue;
1747            }
1748
1749            // Collect dynamic system prompt contribution (config-aware, may read from filesystem)
1750            if let Some(contribution) = capability
1751                .system_prompt_contribution_with_config(ctx, &cap_config.config)
1752                .await
1753            {
1754                system_prompt_attributions.push(SystemPromptAttribution {
1755                    capability_id: cap_id.to_string(),
1756                    content: contribution.clone(),
1757                });
1758                system_prompt_parts.push(contribution);
1759            }
1760
1761            // Collect tools (config-aware: capabilities can adapt based on per-agent config)
1762            tools.extend(capability.tools_with_config(&cap_config.config));
1763            tool_definition_hooks.extend(capability.tool_definition_hooks());
1764            tool_call_hooks.extend(capability.tool_call_hooks());
1765            // Output guardrails are NOT collected here — see CollectedCapabilities
1766            // for rationale. ReasonAtom re-derives them at stream-arming time.
1767
1768            // Collect tool definitions, propagating capability category if not already set
1769            let cap_category = capability.category();
1770            for def in capability.tool_definitions() {
1771                let def = match (def.category(), cap_category) {
1772                    (None, Some(cat)) => def.with_category(cat),
1773                    _ => def,
1774                }
1775                .with_capability_attribution(cap_id, Some(capability.name()));
1776                tool_definitions.push(def);
1777            }
1778
1779            // Detect OpenAI tool_search capability
1780            if cap_id == OPENAI_TOOL_SEARCH_CAPABILITY_ID {
1781                // Parse threshold from config, fall back to default
1782                let threshold = cap_config
1783                    .config
1784                    .get("threshold")
1785                    .and_then(|v| v.as_u64())
1786                    .map(|v| v as usize)
1787                    .unwrap_or(DEFAULT_TOOL_SEARCH_THRESHOLD);
1788                tool_search = Some(crate::llm_driver_registry::ToolSearchConfig {
1789                    enabled: true,
1790                    threshold,
1791                });
1792            }
1793
1794            if cap_id == PROMPT_CACHING_CAPABILITY_ID {
1795                let strategy = cap_config
1796                    .config
1797                    .get("strategy")
1798                    .and_then(|v| v.as_str())
1799                    .map(|value| match value {
1800                        "auto" => crate::llm_driver_registry::PromptCacheStrategy::Auto,
1801                        _ => crate::llm_driver_registry::PromptCacheStrategy::Auto,
1802                    })
1803                    .unwrap_or(crate::llm_driver_registry::PromptCacheStrategy::Auto);
1804                let gemini_cached_content = cap_config
1805                    .config
1806                    .get("gemini_cached_content")
1807                    .and_then(|v| v.as_str())
1808                    .map(str::to_string);
1809                prompt_cache = Some(crate::llm_driver_registry::PromptCacheConfig {
1810                    enabled: true,
1811                    strategy,
1812                    gemini_cached_content,
1813                });
1814            }
1815
1816            // Collect mount points
1817            mounts.extend(capability.mounts());
1818
1819            mcp_servers = merge_scoped_mcp_servers(
1820                &mcp_servers,
1821                &capability.mcp_servers_with_config(&cap_config.config),
1822            );
1823
1824            // Normalize capability-contributed skills into mount points under
1825            // `/.agents/skills/{name}/`. Discovery/activation stays with the
1826            // built-in `skills` capability — see specs/skills-registry.md.
1827            for skill in capability.contribute_skills() {
1828                mounts.push(skill.to_mount(cap_id));
1829            }
1830
1831            // Collect message filter provider
1832            if let Some(provider) = capability.message_filter_provider() {
1833                message_filter_providers.push((provider, cap_config.config.clone()));
1834            }
1835
1836            applied_ids.push(cap_id.to_string());
1837        }
1838    }
1839
1840    // Auto-activate `background_execution` whenever any collected tool
1841    // declares background support via `ToolHints::supports_background`.
1842    //
1843    // This is the generic cross-cutting capability contract — meta-tools that
1844    // wrap other tools based on hints should hook in here, not attach to a
1845    // single owner capability (e.g. `virtual_bash`).
1846    //
1847    // Lockstep: we extend both `tools` (execution registry) and
1848    // `tool_definitions` (model-visible) so the model can see and the worker
1849    // can dispatch `spawn_background` from the same activation event. See
1850    // `specs/background-execution.md`.
1851    if !applied_ids
1852        .iter()
1853        .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID)
1854        && tool_definitions
1855            .iter()
1856            .any(|def| def.hints().supports_background == Some(true))
1857        && let Some(bg_cap) = registry.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
1858        && bg_cap.status() == CapabilityStatus::Available
1859    {
1860        tools.extend(bg_cap.tools());
1861        let cap_category = bg_cap.category();
1862        for def in bg_cap.tool_definitions() {
1863            let def = match (def.category(), cap_category) {
1864                (None, Some(cat)) => def.with_category(cat),
1865                _ => def,
1866            }
1867            .with_capability_attribution(BACKGROUND_EXECUTION_CAPABILITY_ID, Some(bg_cap.name()));
1868            tool_definitions.push(def);
1869        }
1870        applied_ids.push(BACKGROUND_EXECUTION_CAPABILITY_ID.to_string());
1871    }
1872
1873    // Sort message filter providers by priority (lower = earlier)
1874    message_filter_providers.sort_by_key(|(p, _)| p.priority());
1875
1876    CollectedCapabilities {
1877        system_prompt_parts,
1878        system_prompt_attributions,
1879        tools,
1880        tool_definitions,
1881        mounts,
1882        message_filter_providers,
1883        applied_ids,
1884        tool_search,
1885        prompt_cache,
1886        tool_definition_hooks,
1887        tool_call_hooks,
1888        mcp_servers,
1889    }
1890}
1891
1892// ============================================================================
1893// Apply Capabilities to RuntimeAgent
1894// ============================================================================
1895
1896/// Result of applying capabilities to a base runtime agent
1897pub struct AppliedCapabilities {
1898    /// The modified runtime agent with capability contributions merged
1899    pub runtime_agent: RuntimeAgent,
1900    /// Tool registry containing all capability tools
1901    pub tool_registry: ToolRegistry,
1902    /// IDs of capabilities that were applied
1903    pub applied_ids: Vec<String>,
1904}
1905
1906/// Apply capabilities to a base runtime agent configuration.
1907///
1908/// This function:
1909/// 1. Collects system prompt contributions from capabilities (in order)
1910/// 2. Prepends them to the agent's base system prompt
1911/// 3. Collects all tools from capabilities
1912/// 4. Returns the modified runtime agent and a tool registry
1913///
1914/// # Arguments
1915///
1916/// * `base_runtime_agent` - The agent's base runtime configuration
1917/// * `capability_ids` - Ordered list of capability IDs to apply
1918/// * `registry` - The capability registry containing implementations
1919/// * `ctx` - Session context for dynamic prompt resolution
1920///
1921/// # Returns
1922///
1923/// An `AppliedCapabilities` struct containing the modified runtime agent,
1924/// tool registry, and list of applied capability IDs.
1925///
1926/// # Example
1927///
1928/// ```ignore
1929/// use everruns_core::capabilities::{apply_capabilities, CapabilityRegistry, SystemPromptContext};
1930/// use everruns_core::runtime_agent::RuntimeAgent;
1931///
1932/// let registry = CapabilityRegistry::with_builtins();
1933/// let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
1934/// let ctx = SystemPromptContext::without_file_store(SessionId::new());
1935///
1936/// let capability_ids = vec!["current_time".to_string()];
1937/// let applied = apply_capabilities(base_runtime_agent, &capability_ids, &registry, &ctx).await;
1938///
1939/// // The runtime agent now includes CurrentTime tool
1940/// assert!(!applied.tool_registry.is_empty());
1941/// ```
1942pub async fn apply_capabilities(
1943    base_runtime_agent: RuntimeAgent,
1944    capability_ids: &[String],
1945    registry: &CapabilityRegistry,
1946    ctx: &SystemPromptContext,
1947) -> AppliedCapabilities {
1948    let collected = collect_capabilities(capability_ids, registry, ctx).await;
1949
1950    // Build final system prompt: capability additions + base prompt (wrapped in XML tags)
1951    let final_system_prompt = match collected.system_prompt_prefix() {
1952        Some(prefix) => format!(
1953            "{}\n\n<system-prompt>\n{}\n</system-prompt>",
1954            prefix, base_runtime_agent.system_prompt
1955        ),
1956        None => base_runtime_agent.system_prompt,
1957    };
1958
1959    // Build tool registry from collected tools
1960    let mut tool_registry = ToolRegistry::new();
1961    for tool in collected.tools {
1962        tool_registry.register_boxed(tool);
1963    }
1964
1965    // Create modified runtime agent
1966    let mut tools = collected.tool_definitions;
1967    for hook in &collected.tool_definition_hooks {
1968        tools = hook.transform(tools);
1969    }
1970
1971    let runtime_agent = RuntimeAgent {
1972        system_prompt: final_system_prompt,
1973        model: base_runtime_agent.model,
1974        tools,
1975        max_iterations: base_runtime_agent.max_iterations,
1976        temperature: base_runtime_agent.temperature,
1977        max_tokens: base_runtime_agent.max_tokens,
1978        tool_search: collected.tool_search,
1979        prompt_cache: collected.prompt_cache,
1980        network_access: base_runtime_agent.network_access,
1981    };
1982
1983    AppliedCapabilities {
1984        runtime_agent,
1985        tool_registry,
1986        applied_ids: collected.applied_ids,
1987    }
1988}
1989
1990// ============================================================================
1991// Tests
1992// ============================================================================
1993
1994#[cfg(test)]
1995mod tests {
1996    use super::*;
1997    use crate::typed_id::SessionId;
1998    use std::collections::BTreeSet;
1999    use uuid::Uuid;
2000
2001    // Env-var-mutating tests must not run in parallel.
2002    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2003
2004    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
2005        ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
2006    }
2007
2008    /// Test helper: dummy context with no file store
2009    fn test_ctx() -> SystemPromptContext {
2010        SystemPromptContext::without_file_store(SessionId::new())
2011    }
2012
2013    /// Base set of built-in capabilities present in all environments (no experimental delegation).
2014    fn expected_core_builtin_ids() -> BTreeSet<&'static str> {
2015        let mut ids = [
2016            "agent_instructions",
2017            "human_intent",
2018            "budgeting",
2019            "self_budget",
2020            "parallel",
2021            "noop",
2022            "current_time",
2023            "research",
2024            "platform_management",
2025            "session_file_system",
2026            "workspace_volumes",
2027            "session_storage",
2028            "session",
2029            "session_sql_database",
2030            "test_math",
2031            "test_weather",
2032            "stateless_todo_list",
2033            "web_fetch",
2034            "virtual_bash",
2035            "background_execution",
2036            "session_schedule",
2037            "btw",
2038            "infinity_context",
2039            "compaction",
2040            "memory",
2041            "openai_tool_search",
2042            "prompt_caching",
2043            "skills",
2044            "subagents",
2045            "system_commands",
2046            "sample_data",
2047            "data_knowledge",
2048            "knowledge_base",
2049            "tool_output_persistence",
2050            "fake_warehouse",
2051            "fake_aws",
2052            "fake_crm",
2053            "fake_financial",
2054            "loop_detection",
2055            "prompt_canary_guardrail",
2056            "user_hooks",
2057        ]
2058        .into_iter()
2059        .collect::<BTreeSet<_>>();
2060        if cfg!(feature = "ui-capabilities") {
2061            ids.insert("openui");
2062            ids.insert("a2ui");
2063        }
2064        ids
2065    }
2066
2067    /// Full set for dev: base + experimental delegation capabilities.
2068    fn expected_dev_builtin_ids() -> BTreeSet<&'static str> {
2069        let mut ids = expected_core_builtin_ids();
2070        ids.insert("agent_handoff");
2071        ids.insert("a2a_agent_delegation");
2072        ids
2073    }
2074
2075    fn registry_ids(registry: &CapabilityRegistry) -> BTreeSet<&str> {
2076        registry.capabilities.keys().map(String::as_str).collect()
2077    }
2078
2079    // =========================================================================
2080    // CapabilityRegistry tests
2081    // =========================================================================
2082
2083    // Note: Integration plugins (docker, daytona, etc.) are registered via inventory::submit!
2084    // in external crates. They only appear in the registry when the integration crate is
2085    // linked into the final binary. Core tests verify only built-in capabilities.
2086    // Integration crates have their own tests for plugin registration.
2087
2088    #[test]
2089    fn test_capability_registry_with_builtins_dev() {
2090        // Dev mode includes all built-in capabilities including experimental delegation
2091        let _lock = lock_env();
2092        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2093        let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2094        assert_eq!(registry_ids(&registry), expected_dev_builtin_ids());
2095        assert!(registry.has("agent_handoff"));
2096        assert!(registry.has("a2a_agent_delegation"));
2097    }
2098
2099    #[test]
2100    fn test_capability_registry_with_builtins_prod() {
2101        // Prod mode excludes experimental capabilities including delegation
2102        let _lock = lock_env();
2103        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2104        let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2105        assert_eq!(registry_ids(&registry), expected_core_builtin_ids());
2106        // Experimental capabilities NOT included in prod
2107        assert!(!registry.has("docker_container"));
2108        assert!(!registry.has("agent_handoff"));
2109        assert!(!registry.has("a2a_agent_delegation"));
2110    }
2111
2112    #[test]
2113    fn test_agent_delegation_enabled_by_env_in_prod() {
2114        // FEATURE_AGENT_DELEGATION=true enables delegation caps even in prod
2115        let _lock = lock_env();
2116        unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
2117        let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Prod);
2118        assert!(registry.has("agent_handoff"));
2119        assert!(registry.has("a2a_agent_delegation"));
2120        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2121    }
2122
2123    #[test]
2124    fn test_agent_delegation_disabled_by_env_in_dev() {
2125        // FEATURE_AGENT_DELEGATION=false disables delegation caps even in dev
2126        let _lock = lock_env();
2127        unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "false") };
2128        let registry = CapabilityRegistry::with_builtins_for_grade(DeploymentGrade::Dev);
2129        assert!(!registry.has("agent_handoff"));
2130        assert!(!registry.has("a2a_agent_delegation"));
2131        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
2132    }
2133
2134    #[test]
2135    fn test_capability_registry_get() {
2136        let registry = CapabilityRegistry::with_builtins();
2137
2138        let noop = registry.get("noop").unwrap();
2139        assert_eq!(noop.id(), "noop");
2140        assert_eq!(noop.name(), "No-Op");
2141        assert_eq!(noop.status(), CapabilityStatus::Available);
2142    }
2143
2144    #[test]
2145    fn test_capability_registry_blueprint_with_capability() {
2146        struct BlueprintProviderCapability;
2147
2148        impl Capability for BlueprintProviderCapability {
2149            fn id(&self) -> &str {
2150                "blueprint_provider"
2151            }
2152            fn name(&self) -> &str {
2153                "Blueprint Provider"
2154            }
2155            fn description(&self) -> &str {
2156                "Capability that provides a blueprint for tests"
2157            }
2158            fn agent_blueprints(&self) -> Vec<AgentBlueprint> {
2159                vec![AgentBlueprint {
2160                    id: "test_blueprint",
2161                    name: "Test Blueprint",
2162                    description: "Blueprint for capability registry tests",
2163                    model: BlueprintModel::Inherit,
2164                    system_prompt: "Test prompt",
2165                    tools: vec![],
2166                    max_turns: None,
2167                    config_schema: None,
2168                }]
2169            }
2170        }
2171
2172        let mut registry = CapabilityRegistry::new();
2173        registry.register(BlueprintProviderCapability);
2174
2175        let (capability_id, blueprint) = registry
2176            .blueprint_with_capability("test_blueprint")
2177            .expect("blueprint should resolve with capability id");
2178        assert_eq!(capability_id, "blueprint_provider");
2179        assert_eq!(blueprint.id, "test_blueprint");
2180    }
2181
2182    #[test]
2183    fn test_capability_registry_builder() {
2184        let registry = CapabilityRegistry::builder()
2185            .capability(NoopCapability)
2186            .capability(CurrentTimeCapability)
2187            .build();
2188
2189        assert!(registry.has("noop"));
2190        assert!(registry.has("current_time"));
2191        assert_eq!(registry.len(), 2);
2192    }
2193
2194    #[test]
2195    fn test_capability_status() {
2196        let registry = CapabilityRegistry::with_builtins();
2197
2198        let current_time = registry.get("current_time").unwrap();
2199        assert_eq!(current_time.status(), CapabilityStatus::Available);
2200
2201        let research = registry.get("research").unwrap();
2202        assert_eq!(research.status(), CapabilityStatus::ComingSoon);
2203    }
2204
2205    #[test]
2206    fn test_capability_icons_and_categories() {
2207        let registry = CapabilityRegistry::with_builtins();
2208
2209        let noop = registry.get("noop").unwrap();
2210        assert_eq!(noop.icon(), Some("circle-off"));
2211        assert_eq!(noop.category(), Some("Testing"));
2212
2213        let current_time = registry.get("current_time").unwrap();
2214        assert_eq!(current_time.icon(), Some("clock"));
2215        assert_eq!(current_time.category(), Some("Utilities"));
2216    }
2217
2218    #[test]
2219    fn test_system_prompt_preview_default_delegates_to_addition() {
2220        let registry = CapabilityRegistry::with_builtins();
2221
2222        // test_math has a static system_prompt_addition — preview should match
2223        let test_math = registry.get("test_math").unwrap();
2224        assert_eq!(
2225            test_math.system_prompt_preview().as_deref(),
2226            test_math.system_prompt_addition()
2227        );
2228
2229        // current_time has no system_prompt_addition — preview should be None
2230        let current_time = registry.get("current_time").unwrap();
2231        assert!(current_time.system_prompt_preview().is_none());
2232        assert!(current_time.system_prompt_addition().is_none());
2233    }
2234
2235    #[test]
2236    fn test_system_prompt_preview_dynamic_capability() {
2237        let registry = CapabilityRegistry::with_builtins();
2238        let cap = registry.get("agent_instructions").unwrap();
2239
2240        // No static addition, but preview exists
2241        assert!(cap.system_prompt_addition().is_none());
2242        assert!(cap.system_prompt_preview().is_some());
2243        assert!(cap.system_prompt_preview().unwrap().contains("AGENTS.md"));
2244    }
2245
2246    // =========================================================================
2247    // apply_capabilities tests
2248    // =========================================================================
2249
2250    #[tokio::test]
2251    async fn test_apply_capabilities_empty() {
2252        let registry = CapabilityRegistry::with_builtins();
2253        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2254
2255        let applied =
2256            apply_capabilities(base_runtime_agent.clone(), &[], &registry, &test_ctx()).await;
2257
2258        assert_eq!(
2259            applied.runtime_agent.system_prompt,
2260            base_runtime_agent.system_prompt
2261        );
2262        assert!(applied.tool_registry.is_empty());
2263        assert!(applied.applied_ids.is_empty());
2264    }
2265
2266    #[tokio::test]
2267    async fn test_apply_capabilities_noop() {
2268        let registry = CapabilityRegistry::with_builtins();
2269        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2270
2271        let applied = apply_capabilities(
2272            base_runtime_agent.clone(),
2273            &["noop".to_string()],
2274            &registry,
2275            &test_ctx(),
2276        )
2277        .await;
2278
2279        // Noop has no system prompt addition or tools
2280        assert_eq!(
2281            applied.runtime_agent.system_prompt,
2282            base_runtime_agent.system_prompt
2283        );
2284        assert!(applied.tool_registry.is_empty());
2285        assert_eq!(applied.applied_ids, vec!["noop"]);
2286    }
2287
2288    #[tokio::test]
2289    async fn test_apply_capabilities_current_time() {
2290        let registry = CapabilityRegistry::with_builtins();
2291        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2292
2293        let applied = apply_capabilities(
2294            base_runtime_agent.clone(),
2295            &["current_time".to_string()],
2296            &registry,
2297            &test_ctx(),
2298        )
2299        .await;
2300
2301        // CurrentTime has no system prompt addition but has a tool
2302        assert_eq!(
2303            applied.runtime_agent.system_prompt,
2304            base_runtime_agent.system_prompt
2305        );
2306        assert!(applied.tool_registry.has("get_current_time"));
2307        assert_eq!(applied.tool_registry.len(), 1);
2308        assert_eq!(applied.applied_ids, vec!["current_time"]);
2309    }
2310
2311    #[tokio::test]
2312    async fn test_apply_capabilities_skips_coming_soon() {
2313        let registry = CapabilityRegistry::with_builtins();
2314        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2315
2316        // Research is ComingSoon, so it should be skipped
2317        let applied = apply_capabilities(
2318            base_runtime_agent.clone(),
2319            &["research".to_string()],
2320            &registry,
2321            &test_ctx(),
2322        )
2323        .await;
2324
2325        // System prompt should not have the research addition
2326        assert_eq!(
2327            applied.runtime_agent.system_prompt,
2328            base_runtime_agent.system_prompt
2329        );
2330        assert!(applied.applied_ids.is_empty()); // Research was not applied
2331    }
2332
2333    #[tokio::test]
2334    async fn test_apply_capabilities_multiple() {
2335        let registry = CapabilityRegistry::with_builtins();
2336        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2337
2338        let applied = apply_capabilities(
2339            base_runtime_agent.clone(),
2340            &["noop".to_string(), "current_time".to_string()],
2341            &registry,
2342            &test_ctx(),
2343        )
2344        .await;
2345
2346        assert!(applied.tool_registry.has("get_current_time"));
2347        assert_eq!(applied.applied_ids, vec!["noop", "current_time"]);
2348    }
2349
2350    #[tokio::test]
2351    async fn test_apply_capabilities_preserves_order() {
2352        let registry = CapabilityRegistry::with_builtins();
2353        let base_runtime_agent = RuntimeAgent::new("Base prompt.", "gpt-5.2");
2354
2355        // Order should be preserved in applied_ids
2356        let applied = apply_capabilities(
2357            base_runtime_agent,
2358            &["current_time".to_string(), "noop".to_string()],
2359            &registry,
2360            &test_ctx(),
2361        )
2362        .await;
2363
2364        assert_eq!(applied.applied_ids, vec!["current_time", "noop"]);
2365    }
2366
2367    #[tokio::test]
2368    async fn test_apply_capabilities_test_math() {
2369        let registry = CapabilityRegistry::with_builtins();
2370        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2371
2372        let applied = apply_capabilities(
2373            base_runtime_agent.clone(),
2374            &["test_math".to_string()],
2375            &registry,
2376            &test_ctx(),
2377        )
2378        .await;
2379
2380        // TestMath has no system prompt addition (tool defs are sufficient)
2381        assert!(
2382            !applied
2383                .runtime_agent
2384                .system_prompt
2385                .contains("<capability id=\"test_math\">")
2386        );
2387        // No capability prompt prefix, so base prompt is used as-is (no XML wrapping)
2388        assert!(
2389            applied
2390                .runtime_agent
2391                .system_prompt
2392                .contains("You are a helpful assistant.")
2393        );
2394        assert!(applied.tool_registry.has("add"));
2395        assert!(applied.tool_registry.has("subtract"));
2396        assert!(applied.tool_registry.has("multiply"));
2397        assert!(applied.tool_registry.has("divide"));
2398        assert_eq!(applied.tool_registry.len(), 4);
2399    }
2400
2401    #[tokio::test]
2402    async fn test_apply_capabilities_test_weather() {
2403        let registry = CapabilityRegistry::with_builtins();
2404        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2405
2406        let applied = apply_capabilities(
2407            base_runtime_agent.clone(),
2408            &["test_weather".to_string()],
2409            &registry,
2410            &test_ctx(),
2411        )
2412        .await;
2413
2414        // TestWeather has no system prompt addition (tool defs are sufficient)
2415        assert!(
2416            !applied
2417                .runtime_agent
2418                .system_prompt
2419                .contains("<capability id=\"test_weather\">")
2420        );
2421        assert!(applied.tool_registry.has("get_weather"));
2422        assert!(applied.tool_registry.has("get_forecast"));
2423        assert_eq!(applied.tool_registry.len(), 2);
2424    }
2425
2426    #[tokio::test]
2427    async fn test_apply_capabilities_test_math_and_test_weather() {
2428        let registry = CapabilityRegistry::with_builtins();
2429        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2430
2431        let applied = apply_capabilities(
2432            base_runtime_agent.clone(),
2433            &["test_math".to_string(), "test_weather".to_string()],
2434            &registry,
2435            &test_ctx(),
2436        )
2437        .await;
2438
2439        // Should have both sets of tools
2440        assert_eq!(applied.tool_registry.len(), 6); // 4 math + 2 weather
2441        assert!(applied.tool_registry.has("add"));
2442        assert!(applied.tool_registry.has("get_weather"));
2443    }
2444
2445    #[tokio::test]
2446    async fn test_apply_capabilities_stateless_todo_list() {
2447        let registry = CapabilityRegistry::with_builtins();
2448        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2449
2450        let applied = apply_capabilities(
2451            base_runtime_agent.clone(),
2452            &["stateless_todo_list".to_string()],
2453            &registry,
2454            &test_ctx(),
2455        )
2456        .await;
2457
2458        // StatelessTodoList has system prompt addition and 1 tool
2459        assert!(
2460            applied
2461                .runtime_agent
2462                .system_prompt
2463                .contains("Task Management")
2464        );
2465        assert!(applied.runtime_agent.system_prompt.contains("write_todos"));
2466        assert!(applied.tool_registry.has("write_todos"));
2467        assert_eq!(applied.tool_registry.len(), 1);
2468    }
2469
2470    #[tokio::test]
2471    async fn test_apply_capabilities_web_fetch() {
2472        let registry = CapabilityRegistry::with_builtins();
2473        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.2");
2474
2475        let applied = apply_capabilities(
2476            base_runtime_agent.clone(),
2477            &["web_fetch".to_string()],
2478            &registry,
2479            &test_ctx(),
2480        )
2481        .await;
2482
2483        // WebFetch has system prompt from fetchkit's TOOL_LLMTXT and 1 tool
2484        assert!(
2485            applied
2486                .runtime_agent
2487                .system_prompt
2488                .contains(&base_runtime_agent.system_prompt)
2489        );
2490        assert!(applied.runtime_agent.system_prompt.contains("web_fetch"));
2491        assert!(applied.tool_registry.has("web_fetch"));
2492        assert_eq!(applied.tool_registry.len(), 1);
2493    }
2494
2495    // =========================================================================
2496    // XML prompt formatting tests
2497    // =========================================================================
2498
2499    #[tokio::test]
2500    async fn test_xml_tags_wrap_capability_prompts() {
2501        let registry = CapabilityRegistry::with_builtins();
2502        let collected =
2503            collect_capabilities(&["stateless_todo_list".to_string()], &registry, &test_ctx())
2504                .await;
2505
2506        assert_eq!(collected.system_prompt_parts.len(), 1);
2507        let part = &collected.system_prompt_parts[0];
2508        assert!(part.starts_with("<capability id=\"stateless_todo_list\">"));
2509        assert!(part.ends_with("</capability>"));
2510        assert!(part.contains("Task Management"));
2511    }
2512
2513    #[tokio::test]
2514    async fn test_xml_tags_multiple_capabilities() {
2515        let registry = CapabilityRegistry::with_builtins();
2516        let collected = collect_capabilities(
2517            &[
2518                "stateless_todo_list".to_string(),
2519                "session_schedule".to_string(),
2520            ],
2521            &registry,
2522            &test_ctx(),
2523        )
2524        .await;
2525
2526        assert_eq!(collected.system_prompt_parts.len(), 2);
2527        assert!(
2528            collected.system_prompt_parts[0].starts_with("<capability id=\"stateless_todo_list\">")
2529        );
2530        assert!(
2531            collected.system_prompt_parts[1].starts_with("<capability id=\"session_schedule\">")
2532        );
2533
2534        let prefix = collected.system_prompt_prefix().unwrap();
2535        // Both capability sections separated by double newline
2536        assert!(prefix.contains("</capability>\n\n<capability"));
2537    }
2538
2539    #[tokio::test]
2540    async fn test_xml_tags_system_prompt_wrapping() {
2541        let registry = CapabilityRegistry::with_builtins();
2542        let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
2543
2544        let applied = apply_capabilities(
2545            base,
2546            &["stateless_todo_list".to_string()],
2547            &registry,
2548            &test_ctx(),
2549        )
2550        .await;
2551
2552        let prompt = &applied.runtime_agent.system_prompt;
2553        // Capability wrapped
2554        assert!(prompt.contains("<capability id=\"stateless_todo_list\">"));
2555        assert!(prompt.contains("</capability>"));
2556        // Base prompt wrapped
2557        assert!(prompt.contains("<system-prompt>\nYou are helpful.\n</system-prompt>"));
2558    }
2559
2560    #[tokio::test]
2561    async fn test_no_xml_wrapping_without_capabilities() {
2562        let registry = CapabilityRegistry::with_builtins();
2563        let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
2564
2565        let applied = apply_capabilities(base, &[], &registry, &test_ctx()).await;
2566
2567        // No capabilities = no XML wrapping (plain base prompt)
2568        assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
2569        assert!(
2570            !applied
2571                .runtime_agent
2572                .system_prompt
2573                .contains("<system-prompt>")
2574        );
2575    }
2576
2577    #[tokio::test]
2578    async fn test_no_xml_wrapping_for_noop_capability() {
2579        let registry = CapabilityRegistry::with_builtins();
2580        let base = RuntimeAgent::new("You are helpful.", "gpt-5.2");
2581
2582        // Noop has no system_prompt_addition, so no XML wrapping should occur
2583        let applied = apply_capabilities(base, &["noop".to_string()], &registry, &test_ctx()).await;
2584
2585        assert_eq!(applied.runtime_agent.system_prompt, "You are helpful.");
2586        assert!(
2587            !applied
2588                .runtime_agent
2589                .system_prompt
2590                .contains("<system-prompt>")
2591        );
2592    }
2593
2594    // =========================================================================
2595    // Mount collection tests
2596    // =========================================================================
2597
2598    #[tokio::test]
2599    async fn test_collect_capabilities_includes_mounts() {
2600        let registry = CapabilityRegistry::with_builtins();
2601
2602        let collected =
2603            collect_capabilities(&["sample_data".to_string()], &registry, &test_ctx()).await;
2604
2605        assert!(!collected.mounts.is_empty());
2606        assert_eq!(collected.mounts.len(), 1);
2607        assert_eq!(collected.mounts[0].path, "/samples");
2608        assert!(collected.mounts[0].is_readonly());
2609    }
2610
2611    #[tokio::test]
2612    async fn test_collect_capabilities_empty_mounts_by_default() {
2613        let registry = CapabilityRegistry::with_builtins();
2614
2615        // Most capabilities don't have mounts
2616        let collected =
2617            collect_capabilities(&["current_time".to_string()], &registry, &test_ctx()).await;
2618
2619        assert!(collected.mounts.is_empty());
2620    }
2621
2622    #[tokio::test]
2623    async fn test_collect_capabilities_combines_mounts() {
2624        let registry = CapabilityRegistry::with_builtins();
2625
2626        // Collect from multiple capabilities - only sample_data has mounts.
2627        // sample_data depends on session_file_system, which is auto-resolved.
2628        let collected = collect_capabilities(
2629            &["sample_data".to_string(), "current_time".to_string()],
2630            &registry,
2631            &test_ctx(),
2632        )
2633        .await;
2634
2635        assert_eq!(collected.mounts.len(), 1);
2636        // Verify expected capabilities were applied (including auto-resolved dependency)
2637        assert!(
2638            collected
2639                .applied_ids
2640                .iter()
2641                .any(|id| id == "session_file_system")
2642        );
2643        assert!(collected.applied_ids.iter().any(|id| id == "sample_data"));
2644        assert!(collected.applied_ids.iter().any(|id| id == "current_time"));
2645    }
2646
2647    #[test]
2648    fn test_sample_data_capability() {
2649        let registry = CapabilityRegistry::with_builtins();
2650        let cap = registry.get("sample_data").unwrap();
2651
2652        assert_eq!(cap.id(), "sample_data");
2653        assert_eq!(cap.name(), "Sample Data");
2654        assert_eq!(cap.status(), CapabilityStatus::Available);
2655
2656        // Has system prompt but no tools
2657        assert!(cap.system_prompt_addition().is_some());
2658        assert!(cap.tools().is_empty());
2659
2660        // Has mounts
2661        assert!(!cap.mounts().is_empty());
2662    }
2663
2664    // =========================================================================
2665    // Dependency resolution tests
2666    // =========================================================================
2667
2668    #[test]
2669    fn test_resolve_dependencies_empty() {
2670        let registry = CapabilityRegistry::with_builtins();
2671
2672        let resolved = resolve_dependencies(&[], &registry).unwrap();
2673
2674        assert!(resolved.resolved_ids.is_empty());
2675        assert!(resolved.added_as_dependencies.is_empty());
2676        assert!(resolved.user_selected.is_empty());
2677    }
2678
2679    #[test]
2680    fn test_resolve_dependencies_no_deps() {
2681        let registry = CapabilityRegistry::with_builtins();
2682
2683        // CurrentTime has no dependencies
2684        let resolved = resolve_dependencies(&["current_time".to_string()], &registry).unwrap();
2685
2686        assert_eq!(resolved.resolved_ids, vec!["current_time"]);
2687        assert!(resolved.added_as_dependencies.is_empty());
2688    }
2689
2690    #[test]
2691    fn test_resolve_dependencies_with_deps() {
2692        let registry = CapabilityRegistry::with_builtins();
2693
2694        // SampleData depends on FileSystem
2695        let resolved = resolve_dependencies(&["sample_data".to_string()], &registry).unwrap();
2696
2697        // FileSystem should be resolved before SampleData
2698        assert_eq!(resolved.resolved_ids.len(), 2);
2699        let fs_pos = resolved
2700            .resolved_ids
2701            .iter()
2702            .position(|id| id == "session_file_system")
2703            .unwrap();
2704        let sd_pos = resolved
2705            .resolved_ids
2706            .iter()
2707            .position(|id| id == "sample_data")
2708            .unwrap();
2709        assert!(fs_pos < sd_pos, "FileSystem should come before SampleData");
2710
2711        // FileSystem was added as a dependency
2712        assert_eq!(resolved.added_as_dependencies, vec!["session_file_system"]);
2713    }
2714
2715    #[test]
2716    fn test_resolve_dependencies_already_selected() {
2717        let registry = CapabilityRegistry::with_builtins();
2718
2719        // If dependency is already selected, it shouldn't be duplicated
2720        let resolved = resolve_dependencies(
2721            &["session_file_system".to_string(), "sample_data".to_string()],
2722            &registry,
2723        )
2724        .unwrap();
2725
2726        assert_eq!(resolved.resolved_ids.len(), 2);
2727        // FileSystem was user-selected, not added as dependency
2728        assert!(resolved.added_as_dependencies.is_empty());
2729    }
2730
2731    #[test]
2732    fn test_resolve_dependencies_preserves_order() {
2733        let registry = CapabilityRegistry::with_builtins();
2734
2735        // Multiple independent capabilities should maintain their relative order
2736        let resolved =
2737            resolve_dependencies(&["current_time".to_string(), "noop".to_string()], &registry)
2738                .unwrap();
2739
2740        assert_eq!(resolved.resolved_ids, vec!["current_time", "noop"]);
2741    }
2742
2743    #[test]
2744    fn test_resolve_dependencies_unknown_capability() {
2745        let registry = CapabilityRegistry::with_builtins();
2746
2747        // Unknown capabilities are silently skipped
2748        let resolved =
2749            resolve_dependencies(&["unknown_capability".to_string()], &registry).unwrap();
2750
2751        assert!(resolved.resolved_ids.is_empty());
2752    }
2753
2754    #[test]
2755    fn test_get_dependencies() {
2756        let registry = CapabilityRegistry::with_builtins();
2757
2758        // SampleData depends on FileSystem
2759        let deps = get_dependencies("sample_data", &registry);
2760        assert_eq!(deps, vec!["session_file_system"]);
2761
2762        // CurrentTime has no dependencies
2763        let deps = get_dependencies("current_time", &registry);
2764        assert!(deps.is_empty());
2765
2766        // Unknown capability
2767        let deps = get_dependencies("unknown", &registry);
2768        assert!(deps.is_empty());
2769    }
2770
2771    #[test]
2772    fn test_sample_data_has_dependency() {
2773        let registry = CapabilityRegistry::with_builtins();
2774        let cap = registry.get("sample_data").unwrap();
2775
2776        let deps = cap.dependencies();
2777        assert_eq!(deps.len(), 1);
2778        assert_eq!(deps[0], "session_file_system");
2779    }
2780
2781    #[test]
2782    fn test_noop_has_no_dependencies() {
2783        let registry = CapabilityRegistry::with_builtins();
2784        let cap = registry.get("noop").unwrap();
2785
2786        assert!(cap.dependencies().is_empty());
2787    }
2788
2789    // Test for circular dependency detection
2790    // Note: We can't easily test this with built-in capabilities since they don't have cycles.
2791    // This test uses a custom registry to create a cycle.
2792    #[test]
2793    fn test_circular_dependency_error() {
2794        // Create capabilities that form a cycle: A -> B -> A
2795        struct CapA;
2796        struct CapB;
2797
2798        impl Capability for CapA {
2799            fn id(&self) -> &str {
2800                "test_cap_a"
2801            }
2802            fn name(&self) -> &str {
2803                "Test A"
2804            }
2805            fn description(&self) -> &str {
2806                "Test capability A"
2807            }
2808            fn dependencies(&self) -> Vec<&'static str> {
2809                vec!["test_cap_b"]
2810            }
2811        }
2812
2813        impl Capability for CapB {
2814            fn id(&self) -> &str {
2815                "test_cap_b"
2816            }
2817            fn name(&self) -> &str {
2818                "Test B"
2819            }
2820            fn description(&self) -> &str {
2821                "Test capability B"
2822            }
2823            fn dependencies(&self) -> Vec<&'static str> {
2824                vec!["test_cap_a"]
2825            }
2826        }
2827
2828        let mut registry = CapabilityRegistry::new();
2829        registry.register(CapA);
2830        registry.register(CapB);
2831
2832        let result = resolve_dependencies(&["test_cap_a".to_string()], &registry);
2833
2834        assert!(result.is_err());
2835        match result.unwrap_err() {
2836            DependencyError::CircularDependency { capability_id, .. } => {
2837                assert_eq!(capability_id, "test_cap_a");
2838            }
2839            _ => panic!("Expected CircularDependency error"),
2840        }
2841    }
2842
2843    // =========================================================================
2844    // Message filter provider tests
2845    // =========================================================================
2846
2847    use crate::message_filter::{MessageFilter, MessageFilterProvider, MessageQuery};
2848
2849    /// Test capability that provides a message filter
2850    struct FilterTestCapability {
2851        priority: i32,
2852    }
2853
2854    impl Capability for FilterTestCapability {
2855        fn id(&self) -> &str {
2856            "filter_test"
2857        }
2858        fn name(&self) -> &str {
2859            "Filter Test"
2860        }
2861        fn description(&self) -> &str {
2862            "Test capability with message filter"
2863        }
2864        fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
2865            Some(Arc::new(FilterTestProvider {
2866                priority: self.priority,
2867            }))
2868        }
2869    }
2870
2871    struct FilterTestProvider {
2872        priority: i32,
2873    }
2874
2875    impl MessageFilterProvider for FilterTestProvider {
2876        fn apply_filters(&self, query: &mut MessageQuery, config: &serde_json::Value) {
2877            // Add a search filter based on config
2878            if let Some(search) = config.get("search").and_then(|v| v.as_str()) {
2879                query
2880                    .filters
2881                    .push(MessageFilter::Search(search.to_string()));
2882            }
2883        }
2884
2885        fn priority(&self) -> i32 {
2886            self.priority
2887        }
2888    }
2889
2890    #[tokio::test]
2891    async fn test_collect_capabilities_with_configs_no_filter_providers() {
2892        let registry = CapabilityRegistry::with_builtins();
2893        let configs = vec![AgentCapabilityConfig {
2894            capability_ref: CapabilityId::new("current_time"),
2895            config: serde_json::json!({}),
2896        }];
2897
2898        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
2899
2900        assert!(collected.message_filter_providers.is_empty());
2901        assert!(!collected.has_message_filters());
2902    }
2903
2904    #[tokio::test]
2905    async fn test_collect_capabilities_with_configs_with_filter_provider() {
2906        let mut registry = CapabilityRegistry::new();
2907        registry.register(FilterTestCapability { priority: 0 });
2908
2909        let configs = vec![AgentCapabilityConfig {
2910            capability_ref: CapabilityId::new("filter_test"),
2911            config: serde_json::json!({ "search": "hello" }),
2912        }];
2913
2914        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
2915
2916        assert_eq!(collected.message_filter_providers.len(), 1);
2917        assert!(collected.has_message_filters());
2918    }
2919
2920    #[tokio::test]
2921    async fn test_collect_capabilities_with_configs_filter_priority_order() {
2922        // Create capabilities with different priorities
2923        struct HighPriorityCapability;
2924        struct LowPriorityCapability;
2925
2926        impl Capability for HighPriorityCapability {
2927            fn id(&self) -> &str {
2928                "high_priority"
2929            }
2930            fn name(&self) -> &str {
2931                "High Priority"
2932            }
2933            fn description(&self) -> &str {
2934                "Test"
2935            }
2936            fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
2937                Some(Arc::new(FilterTestProvider { priority: 10 }))
2938            }
2939        }
2940
2941        impl Capability for LowPriorityCapability {
2942            fn id(&self) -> &str {
2943                "low_priority"
2944            }
2945            fn name(&self) -> &str {
2946                "Low Priority"
2947            }
2948            fn description(&self) -> &str {
2949                "Test"
2950            }
2951            fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
2952                Some(Arc::new(FilterTestProvider { priority: -5 }))
2953            }
2954        }
2955
2956        let mut registry = CapabilityRegistry::new();
2957        registry.register(HighPriorityCapability);
2958        registry.register(LowPriorityCapability);
2959
2960        // Add in order: high priority first, low priority second
2961        let configs = vec![
2962            AgentCapabilityConfig {
2963                capability_ref: CapabilityId::new("high_priority"),
2964                config: serde_json::json!({}),
2965            },
2966            AgentCapabilityConfig {
2967                capability_ref: CapabilityId::new("low_priority"),
2968                config: serde_json::json!({}),
2969            },
2970        ];
2971
2972        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
2973
2974        // Should be sorted by priority (lower first)
2975        assert_eq!(collected.message_filter_providers.len(), 2);
2976        assert_eq!(collected.message_filter_providers[0].0.priority(), -5);
2977        assert_eq!(collected.message_filter_providers[1].0.priority(), 10);
2978    }
2979
2980    #[tokio::test]
2981    async fn test_collected_capabilities_apply_message_filters() {
2982        let mut registry = CapabilityRegistry::new();
2983        registry.register(FilterTestCapability { priority: 0 });
2984
2985        let configs = vec![AgentCapabilityConfig {
2986            capability_ref: CapabilityId::new("filter_test"),
2987            config: serde_json::json!({ "search": "test_query" }),
2988        }];
2989
2990        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
2991
2992        // Apply filters to a query
2993        let session_id: SessionId = Uuid::now_v7().into();
2994        let mut query = MessageQuery::new(session_id);
2995
2996        collected.apply_message_filters(&mut query);
2997
2998        // Should have added the search filter
2999        assert_eq!(query.filters.len(), 1);
3000        assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3001    }
3002
3003    #[tokio::test]
3004    async fn test_collected_capabilities_apply_multiple_filters_in_priority_order() {
3005        struct SearchCapability {
3006            id: &'static str,
3007            search_term: &'static str,
3008            priority: i32,
3009        }
3010
3011        struct SearchProvider {
3012            search_term: &'static str,
3013            priority: i32,
3014        }
3015
3016        impl MessageFilterProvider for SearchProvider {
3017            fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3018                query
3019                    .filters
3020                    .push(MessageFilter::Search(self.search_term.to_string()));
3021            }
3022
3023            fn priority(&self) -> i32 {
3024                self.priority
3025            }
3026        }
3027
3028        impl Capability for SearchCapability {
3029            fn id(&self) -> &str {
3030                self.id
3031            }
3032            fn name(&self) -> &str {
3033                "Search"
3034            }
3035            fn description(&self) -> &str {
3036                "Test"
3037            }
3038            fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3039                Some(Arc::new(SearchProvider {
3040                    search_term: self.search_term,
3041                    priority: self.priority,
3042                }))
3043            }
3044        }
3045
3046        let mut registry = CapabilityRegistry::new();
3047        registry.register(SearchCapability {
3048            id: "cap_a",
3049            search_term: "alpha",
3050            priority: 5,
3051        });
3052        registry.register(SearchCapability {
3053            id: "cap_b",
3054            search_term: "beta",
3055            priority: 1,
3056        });
3057        registry.register(SearchCapability {
3058            id: "cap_c",
3059            search_term: "gamma",
3060            priority: 10,
3061        });
3062
3063        let configs = vec![
3064            AgentCapabilityConfig {
3065                capability_ref: CapabilityId::new("cap_a"),
3066                config: serde_json::json!({}),
3067            },
3068            AgentCapabilityConfig {
3069                capability_ref: CapabilityId::new("cap_b"),
3070                config: serde_json::json!({}),
3071            },
3072            AgentCapabilityConfig {
3073                capability_ref: CapabilityId::new("cap_c"),
3074                config: serde_json::json!({}),
3075            },
3076        ];
3077
3078        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
3079
3080        let session_id: SessionId = Uuid::now_v7().into();
3081        let mut query = MessageQuery::new(session_id);
3082
3083        collected.apply_message_filters(&mut query);
3084
3085        // Filters should be applied in priority order: beta (1), alpha (5), gamma (10)
3086        assert_eq!(query.filters.len(), 3);
3087        assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3088        assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3089        assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3090    }
3091
3092    #[test]
3093    fn test_capability_without_message_filter_returns_none() {
3094        let registry = CapabilityRegistry::with_builtins();
3095
3096        let noop = registry.get("noop").unwrap();
3097        assert!(noop.message_filter_provider().is_none());
3098
3099        let current_time = registry.get("current_time").unwrap();
3100        assert!(current_time.message_filter_provider().is_none());
3101    }
3102
3103    #[tokio::test]
3104    async fn test_collect_capabilities_preserves_config_for_filter_provider() {
3105        let mut registry = CapabilityRegistry::new();
3106        registry.register(FilterTestCapability { priority: 0 });
3107
3108        let test_config = serde_json::json!({
3109            "search": "custom_search",
3110            "extra_field": 42
3111        });
3112
3113        let configs = vec![AgentCapabilityConfig {
3114            capability_ref: CapabilityId::new("filter_test"),
3115            config: test_config.clone(),
3116        }];
3117
3118        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
3119
3120        // Verify the config is preserved
3121        assert_eq!(collected.message_filter_providers.len(), 1);
3122        let (_, stored_config) = &collected.message_filter_providers[0];
3123        assert_eq!(*stored_config, test_config);
3124    }
3125
3126    // =========================================================================
3127    // collect_message_filters_only tests
3128    // =========================================================================
3129
3130    #[test]
3131    fn test_collect_message_filters_only_collects_filters() {
3132        let mut registry = CapabilityRegistry::new();
3133        registry.register(FilterTestCapability { priority: 0 });
3134
3135        let configs = vec![AgentCapabilityConfig {
3136            capability_ref: CapabilityId::new("filter_test"),
3137            config: serde_json::json!({ "search": "test_query" }),
3138        }];
3139
3140        let collected = collect_message_filters_only(&configs, &registry);
3141
3142        let session_id: SessionId = Uuid::now_v7().into();
3143        let mut query = MessageQuery::new(session_id);
3144        collected.apply_message_filters(&mut query);
3145
3146        assert_eq!(query.filters.len(), 1);
3147        assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "test_query"));
3148    }
3149
3150    #[test]
3151    fn test_collect_message_filters_only_skips_unknown_capabilities() {
3152        let registry = CapabilityRegistry::new();
3153
3154        let configs = vec![AgentCapabilityConfig {
3155            capability_ref: CapabilityId::new("nonexistent"),
3156            config: serde_json::json!({}),
3157        }];
3158
3159        let collected = collect_message_filters_only(&configs, &registry);
3160        assert!(collected.message_filter_providers.is_empty());
3161    }
3162
3163    #[test]
3164    fn test_collect_message_filters_only_preserves_priority_order() {
3165        struct PriorityFilterCap {
3166            id: &'static str,
3167            search_term: &'static str,
3168            priority: i32,
3169        }
3170
3171        struct PriorityFilterProvider {
3172            search_term: &'static str,
3173            priority: i32,
3174        }
3175
3176        impl Capability for PriorityFilterCap {
3177            fn id(&self) -> &str {
3178                self.id
3179            }
3180            fn name(&self) -> &str {
3181                self.id
3182            }
3183            fn description(&self) -> &str {
3184                "priority test"
3185            }
3186            fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3187                Some(Arc::new(PriorityFilterProvider {
3188                    search_term: self.search_term,
3189                    priority: self.priority,
3190                }))
3191            }
3192        }
3193
3194        impl MessageFilterProvider for PriorityFilterProvider {
3195            fn apply_filters(&self, query: &mut MessageQuery, _config: &serde_json::Value) {
3196                query
3197                    .filters
3198                    .push(MessageFilter::Search(self.search_term.to_string()));
3199            }
3200            fn priority(&self) -> i32 {
3201                self.priority
3202            }
3203        }
3204
3205        let mut registry = CapabilityRegistry::new();
3206        registry.register(PriorityFilterCap {
3207            id: "gamma",
3208            search_term: "gamma",
3209            priority: 10,
3210        });
3211        registry.register(PriorityFilterCap {
3212            id: "alpha",
3213            search_term: "alpha",
3214            priority: 5,
3215        });
3216        registry.register(PriorityFilterCap {
3217            id: "beta",
3218            search_term: "beta",
3219            priority: 1,
3220        });
3221
3222        let configs = vec![
3223            AgentCapabilityConfig {
3224                capability_ref: CapabilityId::new("gamma"),
3225                config: serde_json::json!({}),
3226            },
3227            AgentCapabilityConfig {
3228                capability_ref: CapabilityId::new("alpha"),
3229                config: serde_json::json!({}),
3230            },
3231            AgentCapabilityConfig {
3232                capability_ref: CapabilityId::new("beta"),
3233                config: serde_json::json!({}),
3234            },
3235        ];
3236
3237        let collected = collect_message_filters_only(&configs, &registry);
3238
3239        let session_id: SessionId = Uuid::now_v7().into();
3240        let mut query = MessageQuery::new(session_id);
3241        collected.apply_message_filters(&mut query);
3242
3243        // Filters should be applied in priority order: beta (1), alpha (5), gamma (10)
3244        assert_eq!(query.filters.len(), 3);
3245        assert!(matches!(&query.filters[0], MessageFilter::Search(s) if s == "beta"));
3246        assert!(matches!(&query.filters[1], MessageFilter::Search(s) if s == "alpha"));
3247        assert!(matches!(&query.filters[2], MessageFilter::Search(s) if s == "gamma"));
3248    }
3249
3250    #[test]
3251    fn test_collect_message_filters_only_post_load_invoked() {
3252        use crate::message::Message;
3253
3254        struct PostLoadCap;
3255        struct PostLoadProvider;
3256
3257        impl Capability for PostLoadCap {
3258            fn id(&self) -> &str {
3259                "post_load_test"
3260            }
3261            fn name(&self) -> &str {
3262                "PostLoad Test"
3263            }
3264            fn description(&self) -> &str {
3265                "test"
3266            }
3267            fn message_filter_provider(&self) -> Option<Arc<dyn MessageFilterProvider>> {
3268                Some(Arc::new(PostLoadProvider))
3269            }
3270        }
3271
3272        impl MessageFilterProvider for PostLoadProvider {
3273            fn apply_filters(&self, _query: &mut MessageQuery, _config: &serde_json::Value) {}
3274            fn priority(&self) -> i32 {
3275                0
3276            }
3277            fn post_load(&self, messages: &mut Vec<Message>, _config: &serde_json::Value) {
3278                // Reverse messages to prove post_load was called
3279                messages.reverse();
3280            }
3281        }
3282
3283        let mut registry = CapabilityRegistry::new();
3284        registry.register(PostLoadCap);
3285
3286        let configs = vec![AgentCapabilityConfig {
3287            capability_ref: CapabilityId::new("post_load_test"),
3288            config: serde_json::json!({}),
3289        }];
3290
3291        let collected = collect_message_filters_only(&configs, &registry);
3292
3293        let mut messages = vec![Message::user("first"), Message::user("second")];
3294        collected.apply_post_load_filters(&mut messages);
3295
3296        // post_load reversed the messages
3297        assert_eq!(messages[0].text(), Some("second"));
3298        assert_eq!(messages[1].text(), Some("first"));
3299    }
3300
3301    #[test]
3302    fn test_collect_model_view_providers_respects_compaction_capability_boundary() {
3303        use crate::tool_types::ToolCall;
3304
3305        fn tool_heavy_messages() -> Vec<Message> {
3306            let mut messages = vec![Message::user("inspect files repeatedly")];
3307            for index in 0..9 {
3308                let call_id = format!("call_{index}");
3309                messages.push(Message::assistant_with_tools(
3310                    "",
3311                    vec![ToolCall {
3312                        id: call_id.clone(),
3313                        name: "read_file".to_string(),
3314                        arguments: serde_json::json!({"path": "/workspace/src/lib.rs"}),
3315                    }],
3316                ));
3317                messages.push(Message::tool_result(
3318                    call_id,
3319                    Some(serde_json::json!({
3320                        "path": "/workspace/src/lib.rs",
3321                        "content": format!("{}{}", "large file line\n".repeat(1000), index),
3322                        "total_lines": 1000,
3323                        "lines_shown": {"start": 1, "end": 1000},
3324                        "truncated": false
3325                    })),
3326                    None,
3327                ));
3328            }
3329            messages
3330        }
3331
3332        fn first_tool_result_is_masked(messages: &[Message]) -> bool {
3333            messages[2]
3334                .tool_result_content()
3335                .and_then(|result| result.result.as_ref())
3336                .and_then(|result| result.get("masked"))
3337                .and_then(|masked| masked.as_bool())
3338                .unwrap_or(false)
3339        }
3340
3341        let mut registry = CapabilityRegistry::new();
3342        registry.register(CompactionCapability);
3343        let context = ModelViewContext {
3344            session_id: SessionId::new(),
3345            prior_usage: None,
3346        };
3347
3348        let no_compaction = collect_model_view_providers(&[], &registry);
3349        let unmasked = no_compaction.apply_model_view(tool_heavy_messages(), &context);
3350        assert!(!first_tool_result_is_masked(&unmasked));
3351
3352        let compaction = collect_model_view_providers(
3353            &[AgentCapabilityConfig {
3354                capability_ref: CapabilityId::new(COMPACTION_CAPABILITY_ID),
3355                config: serde_json::json!({}),
3356            }],
3357            &registry,
3358        );
3359        let masked = compaction.apply_model_view(tool_heavy_messages(), &context);
3360        assert!(first_tool_result_is_masked(&masked));
3361        let last_tool = masked.last().unwrap().tool_result_content().unwrap();
3362        assert!(last_tool.result.as_ref().unwrap().get("content").is_some());
3363    }
3364
3365    // =========================================================================
3366    // Harness capability tool registration tests
3367    //
3368    // Regression tests for the "Tool not found: bash" bug where harness
3369    // capabilities were not used for tool registration when agent_id was absent.
3370    // These tests verify that capability-provided tools (especially bash) are
3371    // correctly produced by collect_capabilities.
3372    // =========================================================================
3373
3374    #[tokio::test]
3375    async fn test_virtual_bash_capability_produces_bash_tool() {
3376        let registry = CapabilityRegistry::with_builtins();
3377        let collected =
3378            collect_capabilities(&["virtual_bash".to_string()], &registry, &test_ctx()).await;
3379
3380        let tool_names: Vec<&str> = collected
3381            .tool_definitions
3382            .iter()
3383            .map(|t| t.name())
3384            .collect();
3385        assert!(
3386            tool_names.contains(&"bash"),
3387            "virtual_bash capability must produce 'bash' tool, got: {:?}",
3388            tool_names
3389        );
3390        assert!(
3391            !collected.tools.is_empty(),
3392            "virtual_bash must provide tool implementations"
3393        );
3394    }
3395
3396    #[tokio::test]
3397    async fn test_generic_harness_capability_set_produces_bash_tool() {
3398        // These are the exact capability IDs from the Generic Harness seed data.
3399        // If any are renamed or removed, this test catches the regression.
3400        let generic_harness_caps = vec![
3401            "session_file_system".to_string(),
3402            "virtual_bash".to_string(),
3403            "web_fetch".to_string(),
3404            "session_storage".to_string(),
3405            "session".to_string(),
3406            "agent_instructions".to_string(),
3407            "skills".to_string(),
3408            "infinity_context".to_string(),
3409            "openai_tool_search".to_string(),
3410        ];
3411
3412        let registry = CapabilityRegistry::with_builtins();
3413        let collected = collect_capabilities(&generic_harness_caps, &registry, &test_ctx()).await;
3414
3415        let tool_names: Vec<&str> = collected
3416            .tool_definitions
3417            .iter()
3418            .map(|t| t.name())
3419            .collect();
3420        assert!(
3421            tool_names.contains(&"bash"),
3422            "Generic Harness capabilities must produce 'bash' tool, got: {:?}",
3423            tool_names
3424        );
3425    }
3426
3427    #[tokio::test]
3428    async fn test_collect_capabilities_tool_count_matches_definitions() {
3429        // Ensure collected tools (implementations) match tool_definitions count.
3430        // A mismatch means some tools won't be executable at runtime.
3431        let registry = CapabilityRegistry::with_builtins();
3432        let collected =
3433            collect_capabilities(&["virtual_bash".to_string()], &registry, &test_ctx()).await;
3434
3435        assert_eq!(
3436            collected.tools.len(),
3437            collected.tool_definitions.len(),
3438            "tool implementations ({}) must match tool definitions ({})",
3439            collected.tools.len(),
3440            collected.tool_definitions.len(),
3441        );
3442    }
3443
3444    /// Regression test for EVE-189: collect_capabilities must resolve dependencies
3445    /// so that transitive capabilities register their tools even when not explicitly
3446    /// listed. Uses sample_data (depends on session_file_system) as the test case.
3447    #[tokio::test]
3448    async fn test_collect_capabilities_resolves_dependencies() {
3449        // sample_data depends on session_file_system
3450        // Passing only sample_data should still include session_file_system tools
3451        let registry = CapabilityRegistry::with_builtins();
3452        let collected =
3453            collect_capabilities(&["sample_data".to_string()], &registry, &test_ctx()).await;
3454
3455        // Verify the transitive dependency capability itself was applied
3456        assert!(
3457            collected
3458                .applied_ids
3459                .iter()
3460                .any(|id| id == "session_file_system"),
3461            "collect_capabilities must apply session_file_system as a dependency; applied_ids: {:?}",
3462            collected.applied_ids
3463        );
3464
3465        let tool_names: Vec<&str> = collected
3466            .tool_definitions
3467            .iter()
3468            .map(|t| t.name())
3469            .collect();
3470
3471        // session_file_system provides these tools; both should be present
3472        assert!(
3473            tool_names.contains(&"read_file") && tool_names.contains(&"write_file"),
3474            "collect_capabilities must resolve dependencies and include dependency tools, got: {:?}",
3475            tool_names
3476        );
3477
3478        // Also verify tool implementations match definitions (dependency tools are executable)
3479        assert_eq!(
3480            collected.tools.len(),
3481            collected.tool_definitions.len(),
3482            "dependency-added tools must have implementations, not just definitions"
3483        );
3484    }
3485
3486    #[test]
3487    fn test_defaults_do_not_include_bash() {
3488        // ToolRegistry::with_defaults() must NOT include bash — it comes from
3489        // capabilities only. This documents the invariant that the bug violated.
3490        let registry = crate::ToolRegistry::with_defaults();
3491        assert!(
3492            !registry.has("bash"),
3493            "with_defaults() must not include 'bash' — it comes from virtual_bash capability"
3494        );
3495    }
3496
3497    // =========================================================================
3498    // EVE-501: background_execution auto-activation
3499    // =========================================================================
3500
3501    /// Auto-activation: any collected tool with `supports_background=true`
3502    /// causes `spawn_background` to appear in both tool_definitions and tools.
3503    #[tokio::test]
3504    async fn test_background_execution_auto_activates_with_virtual_bash() {
3505        let registry = CapabilityRegistry::with_builtins();
3506        let collected =
3507            collect_capabilities(&["virtual_bash".to_string()], &registry, &test_ctx()).await;
3508
3509        let tool_names: Vec<&str> = collected
3510            .tool_definitions
3511            .iter()
3512            .map(|t| t.name())
3513            .collect();
3514        assert!(
3515            tool_names.contains(&"spawn_background"),
3516            "spawn_background must be auto-activated when virtual_bash (a \
3517             background-capable tool) is in the agent's capability set; got: {:?}",
3518            tool_names
3519        );
3520        assert!(
3521            collected
3522                .applied_ids
3523                .iter()
3524                .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
3525            "background_execution must be in applied_ids when auto-activated; \
3526             got: {:?}",
3527            collected.applied_ids
3528        );
3529
3530        // Lockstep: implementations match definitions (executable in the worker).
3531        assert!(
3532            collected
3533                .tools
3534                .iter()
3535                .any(|t| t.name() == "spawn_background"),
3536            "spawn_background tool implementation must be present alongside the \
3537             definition (lockstep contract)"
3538        );
3539    }
3540
3541    /// Negative: when no collected tool declares background support, the
3542    /// capability must NOT auto-activate.
3543    #[tokio::test]
3544    async fn test_background_execution_does_not_auto_activate_without_hint() {
3545        let registry = CapabilityRegistry::with_builtins();
3546        // current_time has no background-capable tool.
3547        let collected =
3548            collect_capabilities(&["current_time".to_string()], &registry, &test_ctx()).await;
3549
3550        let tool_names: Vec<&str> = collected
3551            .tool_definitions
3552            .iter()
3553            .map(|t| t.name())
3554            .collect();
3555        assert!(
3556            !tool_names.contains(&"spawn_background"),
3557            "spawn_background must NOT be activated without a background-capable \
3558             tool; got: {:?}",
3559            tool_names
3560        );
3561        assert!(
3562            !collected
3563                .applied_ids
3564                .iter()
3565                .any(|id| id == BACKGROUND_EXECUTION_CAPABILITY_ID),
3566            "background_execution must not appear in applied_ids when no \
3567             background-capable tool is present; got: {:?}",
3568            collected.applied_ids
3569        );
3570    }
3571
3572    /// Idempotence: explicitly selecting `background_execution` plus a
3573    /// background-capable tool must not produce duplicate spawn_background
3574    /// entries.
3575    #[tokio::test]
3576    async fn test_background_execution_explicit_selection_is_idempotent() {
3577        let registry = CapabilityRegistry::with_builtins();
3578        let collected = collect_capabilities(
3579            &[
3580                "virtual_bash".to_string(),
3581                BACKGROUND_EXECUTION_CAPABILITY_ID.to_string(),
3582            ],
3583            &registry,
3584            &test_ctx(),
3585        )
3586        .await;
3587
3588        let spawn_background_count = collected
3589            .tool_definitions
3590            .iter()
3591            .filter(|t| t.name() == "spawn_background")
3592            .count();
3593        assert_eq!(
3594            spawn_background_count, 1,
3595            "spawn_background must appear exactly once even when \
3596             background_execution is selected explicitly alongside a \
3597             background-capable tool"
3598        );
3599        let applied_count = collected
3600            .applied_ids
3601            .iter()
3602            .filter(|id| id.as_str() == BACKGROUND_EXECUTION_CAPABILITY_ID)
3603            .count();
3604        assert_eq!(
3605            applied_count, 1,
3606            "background_execution must appear exactly once in applied_ids"
3607        );
3608    }
3609
3610    /// Lockstep: with_defaults() must NOT include spawn_background — it only
3611    /// reaches the worker registry through the auto-activated capability.
3612    /// This proves the executor cannot dispatch spawn_background without the
3613    /// model having seen it.
3614    #[test]
3615    fn test_defaults_do_not_include_spawn_background() {
3616        let registry = crate::ToolRegistry::with_defaults();
3617        assert!(
3618            !registry.has("spawn_background"),
3619            "with_defaults() must not include 'spawn_background' — it comes \
3620             from the background_execution capability (EVE-501)"
3621        );
3622    }
3623
3624    // =========================================================================
3625    // Feature tests
3626    // =========================================================================
3627
3628    #[test]
3629    fn test_capability_features_default_empty() {
3630        let registry = CapabilityRegistry::with_builtins();
3631
3632        // Most capabilities have no features
3633        let noop = registry.get("noop").unwrap();
3634        assert!(noop.features().is_empty());
3635
3636        let current_time = registry.get("current_time").unwrap();
3637        assert!(current_time.features().is_empty());
3638    }
3639
3640    #[test]
3641    fn test_file_system_capability_features() {
3642        let registry = CapabilityRegistry::with_builtins();
3643
3644        let fs = registry.get("session_file_system").unwrap();
3645        assert_eq!(fs.features(), vec!["file_system"]);
3646    }
3647
3648    #[test]
3649    fn test_virtual_bash_capability_features() {
3650        let registry = CapabilityRegistry::with_builtins();
3651
3652        let bash = registry.get("virtual_bash").unwrap();
3653        assert_eq!(bash.features(), vec!["file_system"]);
3654    }
3655
3656    #[test]
3657    fn test_session_storage_capability_features() {
3658        let registry = CapabilityRegistry::with_builtins();
3659
3660        let storage = registry.get("session_storage").unwrap();
3661        let features = storage.features();
3662        assert!(features.contains(&"secrets"));
3663        assert!(features.contains(&"key_value"));
3664    }
3665
3666    #[test]
3667    fn test_session_schedule_capability_features() {
3668        let registry = CapabilityRegistry::with_builtins();
3669
3670        let schedule = registry.get("session_schedule").unwrap();
3671        assert_eq!(schedule.features(), vec!["schedules"]);
3672    }
3673
3674    #[test]
3675    fn test_session_sql_database_capability_features() {
3676        let registry = CapabilityRegistry::with_builtins();
3677
3678        let sql = registry.get("session_sql_database").unwrap();
3679        assert_eq!(sql.features(), vec!["sql_database"]);
3680    }
3681
3682    #[test]
3683    fn test_sample_data_capability_features() {
3684        let registry = CapabilityRegistry::with_builtins();
3685
3686        let sample = registry.get("sample_data").unwrap();
3687        assert_eq!(sample.features(), vec!["file_system"]);
3688    }
3689
3690    #[test]
3691    fn test_compute_features_empty() {
3692        let registry = CapabilityRegistry::with_builtins();
3693
3694        let features = compute_features(&[], &registry);
3695        assert!(features.is_empty());
3696    }
3697
3698    #[test]
3699    fn test_compute_features_single_capability() {
3700        let registry = CapabilityRegistry::with_builtins();
3701
3702        let features = compute_features(&["session_schedule".to_string()], &registry);
3703        assert_eq!(features, vec!["schedules"]);
3704    }
3705
3706    #[test]
3707    fn test_compute_features_multiple_capabilities() {
3708        let registry = CapabilityRegistry::with_builtins();
3709
3710        let features = compute_features(
3711            &[
3712                "session_file_system".to_string(),
3713                "session_storage".to_string(),
3714                "session_schedule".to_string(),
3715            ],
3716            &registry,
3717        );
3718        assert!(features.contains(&"file_system".to_string()));
3719        assert!(features.contains(&"secrets".to_string()));
3720        assert!(features.contains(&"key_value".to_string()));
3721        assert!(features.contains(&"schedules".to_string()));
3722    }
3723
3724    #[test]
3725    fn test_compute_features_deduplicates() {
3726        let registry = CapabilityRegistry::with_builtins();
3727
3728        // Both session_file_system and virtual_bash contribute "file_system"
3729        let features = compute_features(
3730            &[
3731                "session_file_system".to_string(),
3732                "virtual_bash".to_string(),
3733            ],
3734            &registry,
3735        );
3736        let file_system_count = features.iter().filter(|f| *f == "file_system").count();
3737        assert_eq!(file_system_count, 1, "file_system should appear only once");
3738    }
3739
3740    #[test]
3741    fn test_compute_features_includes_dependency_features() {
3742        let registry = CapabilityRegistry::with_builtins();
3743
3744        // virtual_bash depends on session_file_system; both contribute "file_system"
3745        let features = compute_features(&["virtual_bash".to_string()], &registry);
3746        assert!(features.contains(&"file_system".to_string()));
3747    }
3748
3749    #[test]
3750    fn test_compute_features_generic_harness_set() {
3751        let registry = CapabilityRegistry::with_builtins();
3752
3753        // Typical Generic Harness capabilities
3754        let features = compute_features(
3755            &[
3756                "session_file_system".to_string(),
3757                "virtual_bash".to_string(),
3758                "session_storage".to_string(),
3759                "session".to_string(),
3760                "session_schedule".to_string(),
3761            ],
3762            &registry,
3763        );
3764        assert!(features.contains(&"file_system".to_string()));
3765        assert!(features.contains(&"secrets".to_string()));
3766        assert!(features.contains(&"key_value".to_string()));
3767        assert!(features.contains(&"schedules".to_string()));
3768    }
3769
3770    #[test]
3771    fn test_compute_features_unknown_capability_ignored() {
3772        let registry = CapabilityRegistry::with_builtins();
3773
3774        let features = compute_features(
3775            &["unknown_cap".to_string(), "session_schedule".to_string()],
3776            &registry,
3777        );
3778        assert_eq!(features, vec!["schedules"]);
3779    }
3780
3781    #[test]
3782    fn test_risk_level_ordering() {
3783        assert!(RiskLevel::Low < RiskLevel::Medium);
3784        assert!(RiskLevel::Medium < RiskLevel::High);
3785    }
3786
3787    #[test]
3788    fn test_risk_level_serde_roundtrip() {
3789        let high = RiskLevel::High;
3790        let json = serde_json::to_string(&high).unwrap();
3791        assert_eq!(json, "\"high\"");
3792        let back: RiskLevel = serde_json::from_str(&json).unwrap();
3793        assert_eq!(back, RiskLevel::High);
3794    }
3795
3796    #[test]
3797    fn test_capability_risk_levels() {
3798        let registry = CapabilityRegistry::with_builtins();
3799
3800        // virtual_bash is High (code execution requires admin gating)
3801        let bash = registry.get("virtual_bash").unwrap();
3802        assert_eq!(bash.risk_level(), RiskLevel::High);
3803
3804        // web_fetch is High (network access requires admin gating)
3805        let fetch = registry.get("web_fetch").unwrap();
3806        assert_eq!(fetch.risk_level(), RiskLevel::High);
3807
3808        // Default capabilities should be Low
3809        let noop = registry.get("noop").unwrap();
3810        assert_eq!(noop.risk_level(), RiskLevel::Low);
3811    }
3812
3813    // =========================================================================
3814    // OpenAI tool_search capability collection tests
3815    // =========================================================================
3816
3817    #[tokio::test]
3818    async fn test_apply_capabilities_openai_tool_search() {
3819        let registry = CapabilityRegistry::with_builtins();
3820        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
3821
3822        let applied = apply_capabilities(
3823            base_runtime_agent.clone(),
3824            &["openai_tool_search".to_string()],
3825            &registry,
3826            &test_ctx(),
3827        )
3828        .await;
3829
3830        // OpenAiToolSearchCapability provides no tools and no system prompt
3831        assert_eq!(
3832            applied.runtime_agent.system_prompt,
3833            base_runtime_agent.system_prompt
3834        );
3835        assert!(applied.tool_registry.is_empty());
3836        assert_eq!(applied.applied_ids, vec!["openai_tool_search"]);
3837
3838        // tool_search config should be set on the runtime agent
3839        let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
3840        assert!(ts.enabled);
3841        assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
3842    }
3843
3844    #[tokio::test]
3845    async fn test_apply_capabilities_openai_tool_search_with_other_capabilities() {
3846        let registry = CapabilityRegistry::with_builtins();
3847        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
3848
3849        let applied = apply_capabilities(
3850            base_runtime_agent,
3851            &[
3852                "current_time".to_string(),
3853                "openai_tool_search".to_string(),
3854                "test_math".to_string(),
3855            ],
3856            &registry,
3857            &test_ctx(),
3858        )
3859        .await;
3860
3861        // Should have tools from current_time and test_math
3862        assert!(applied.tool_registry.has("get_current_time"));
3863        assert!(applied.tool_registry.has("add"));
3864        assert!(applied.tool_registry.has("subtract"));
3865        assert!(applied.tool_registry.has("multiply"));
3866        assert!(applied.tool_registry.has("divide"));
3867
3868        // tool_search should still be configured
3869        let ts = applied.runtime_agent.tool_search.as_ref().unwrap();
3870        assert!(ts.enabled);
3871        assert_eq!(ts.threshold, DEFAULT_TOOL_SEARCH_THRESHOLD);
3872    }
3873
3874    #[tokio::test]
3875    async fn test_collect_capabilities_tool_search_custom_threshold() {
3876        let registry = CapabilityRegistry::with_builtins();
3877
3878        let configs = vec![AgentCapabilityConfig {
3879            capability_ref: CapabilityId::new("openai_tool_search"),
3880            config: serde_json::json!({"threshold": 5}),
3881        }];
3882
3883        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
3884
3885        let ts = collected.tool_search.as_ref().unwrap();
3886        assert!(ts.enabled);
3887        assert_eq!(ts.threshold, 5);
3888    }
3889
3890    #[tokio::test]
3891    async fn test_collect_capabilities_no_tool_search_without_capability() {
3892        let registry = CapabilityRegistry::with_builtins();
3893
3894        let configs = vec![AgentCapabilityConfig {
3895            capability_ref: CapabilityId::new("current_time"),
3896            config: serde_json::json!({}),
3897        }];
3898
3899        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
3900
3901        assert!(collected.tool_search.is_none());
3902    }
3903
3904    #[tokio::test]
3905    async fn test_collect_capabilities_tool_search_category_propagation() {
3906        let registry = CapabilityRegistry::with_builtins();
3907
3908        // test_math capability has category "Testing"
3909        let configs = vec![
3910            AgentCapabilityConfig {
3911                capability_ref: CapabilityId::new("test_math"),
3912                config: serde_json::json!({}),
3913            },
3914            AgentCapabilityConfig {
3915                capability_ref: CapabilityId::new("openai_tool_search"),
3916                config: serde_json::json!({}),
3917            },
3918        ];
3919
3920        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
3921
3922        // Verify tool_search is configured
3923        assert!(collected.tool_search.is_some());
3924
3925        // Verify tools have categories from their capability
3926        for tool_def in &collected.tool_definitions {
3927            // test_math tools should have the Math category
3928            if ["add", "subtract", "multiply", "divide"].contains(&tool_def.name()) {
3929                assert!(
3930                    tool_def.category().is_some(),
3931                    "Tool {} should have a category from its capability",
3932                    tool_def.name()
3933                );
3934            }
3935        }
3936    }
3937
3938    #[tokio::test]
3939    async fn test_apply_capabilities_prompt_caching() {
3940        let registry = CapabilityRegistry::with_builtins();
3941        let base_runtime_agent = RuntimeAgent::new("You are a helpful assistant.", "gpt-5.4");
3942
3943        let applied = apply_capabilities(
3944            base_runtime_agent.clone(),
3945            &["prompt_caching".to_string()],
3946            &registry,
3947            &test_ctx(),
3948        )
3949        .await;
3950
3951        assert_eq!(
3952            applied.runtime_agent.system_prompt,
3953            base_runtime_agent.system_prompt
3954        );
3955        assert!(applied.tool_registry.is_empty());
3956        assert_eq!(applied.applied_ids, vec!["prompt_caching"]);
3957
3958        let prompt_cache = applied.runtime_agent.prompt_cache.as_ref().unwrap();
3959        assert!(prompt_cache.enabled);
3960        assert_eq!(
3961            prompt_cache.strategy,
3962            crate::llm_driver_registry::PromptCacheStrategy::Auto
3963        );
3964        assert!(prompt_cache.gemini_cached_content.is_none());
3965    }
3966
3967    #[tokio::test]
3968    async fn test_collect_capabilities_prompt_caching_custom_strategy() {
3969        let registry = CapabilityRegistry::with_builtins();
3970
3971        let configs = vec![AgentCapabilityConfig {
3972            capability_ref: CapabilityId::new("prompt_caching"),
3973            config: serde_json::json!({"strategy": "auto"}),
3974        }];
3975
3976        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
3977
3978        let prompt_cache = collected.prompt_cache.as_ref().unwrap();
3979        assert!(prompt_cache.enabled);
3980        assert_eq!(
3981            prompt_cache.strategy,
3982            crate::llm_driver_registry::PromptCacheStrategy::Auto
3983        );
3984        assert!(prompt_cache.gemini_cached_content.is_none());
3985    }
3986
3987    #[tokio::test]
3988    async fn test_collect_capabilities_prompt_caching_gemini_cached_content() {
3989        let registry = CapabilityRegistry::with_builtins();
3990
3991        let configs = vec![AgentCapabilityConfig {
3992            capability_ref: CapabilityId::new("prompt_caching"),
3993            config: serde_json::json!({
3994                "strategy": "auto",
3995                "gemini_cached_content": "cachedContents/demo-cache"
3996            }),
3997        }];
3998
3999        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
4000
4001        let prompt_cache = collected.prompt_cache.as_ref().unwrap();
4002        assert_eq!(
4003            prompt_cache.gemini_cached_content.as_deref(),
4004            Some("cachedContents/demo-cache")
4005        );
4006    }
4007
4008    // ========================================================================
4009    // contribute_skills() collection — EVE-311
4010    // ========================================================================
4011
4012    struct SkillContributingCapability;
4013
4014    impl Capability for SkillContributingCapability {
4015        fn id(&self) -> &str {
4016            "contributes_skills"
4017        }
4018        fn name(&self) -> &str {
4019            "Contributes Skills"
4020        }
4021        fn description(&self) -> &str {
4022            "Test capability that contributes skills."
4023        }
4024        fn contribute_skills(&self) -> Vec<SkillContribution> {
4025            vec![
4026                SkillContribution::new("alpha-skill", "Alpha skill desc", "# Alpha\nDo alpha.")
4027                    .with_files(vec![(
4028                        "scripts/a.sh".to_string(),
4029                        "#!/bin/sh\necho a\n".to_string(),
4030                    )]),
4031                SkillContribution::new("beta-skill", "Beta skill desc", "# Beta\nDo beta.")
4032                    .with_user_invocable(false),
4033            ]
4034        }
4035    }
4036
4037    fn skill_md_from_entries(entries: &HashMap<String, MountEntry>) -> &str {
4038        match &entries.get("SKILL.md").expect("SKILL.md missing").source {
4039            MountSource::InlineFile { content, .. } => content.as_str(),
4040            _ => panic!("Expected InlineFile for SKILL.md"),
4041        }
4042    }
4043
4044    #[tokio::test]
4045    async fn test_contribute_skills_normalized_to_mounts() {
4046        let mut registry = CapabilityRegistry::new();
4047        registry.register(SkillContributingCapability);
4048
4049        let configs = vec![AgentCapabilityConfig {
4050            capability_ref: CapabilityId::new("contributes_skills"),
4051            config: serde_json::json!({}),
4052        }];
4053
4054        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
4055
4056        let skill_mounts: Vec<_> = collected
4057            .mounts
4058            .iter()
4059            .filter(|m| m.path.starts_with("/.agents/skills/"))
4060            .collect();
4061        assert_eq!(skill_mounts.len(), 2);
4062
4063        // Every contributed skill mount is read-only and owned by the contributing
4064        // capability so the VFS layer can attribute skill files correctly.
4065        for m in &skill_mounts {
4066            assert!(m.is_readonly());
4067            assert_eq!(m.capability_id, "contributes_skills");
4068        }
4069
4070        let alpha = skill_mounts
4071            .iter()
4072            .find(|m| m.path == "/.agents/skills/alpha-skill")
4073            .expect("alpha-skill mount missing");
4074        match &alpha.source {
4075            MountSource::InlineDirectory { entries } => {
4076                assert!(entries.contains_key("SKILL.md"));
4077                assert!(entries.contains_key("scripts/a.sh"));
4078                let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
4079                assert_eq!(parsed.name, "alpha-skill");
4080                assert!(parsed.user_invocable);
4081            }
4082            _ => panic!("Expected InlineDirectory"),
4083        }
4084
4085        let beta = skill_mounts
4086            .iter()
4087            .find(|m| m.path == "/.agents/skills/beta-skill")
4088            .expect("beta-skill mount missing");
4089        match &beta.source {
4090            MountSource::InlineDirectory { entries } => {
4091                let parsed = crate::skill::parse_skill_md(skill_md_from_entries(entries)).unwrap();
4092                assert!(!parsed.user_invocable);
4093            }
4094            _ => panic!("Expected InlineDirectory"),
4095        }
4096    }
4097
4098    #[tokio::test]
4099    async fn test_contribute_skills_default_empty() {
4100        // Registry-resident capability without a contribute_skills override
4101        // must not add skill mounts.
4102        let mut registry = CapabilityRegistry::new();
4103        registry.register(FilterTestCapability { priority: 0 });
4104
4105        let configs = vec![AgentCapabilityConfig {
4106            capability_ref: CapabilityId::new("filter_test"),
4107            config: serde_json::json!({}),
4108        }];
4109
4110        let collected = collect_capabilities_with_configs(&configs, &registry, &test_ctx()).await;
4111        assert!(
4112            collected
4113                .mounts
4114                .iter()
4115                .all(|m| !m.path.starts_with("/.agents/skills/"))
4116        );
4117    }
4118}