diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs index 75d45dd75..9c880f524 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs @@ -40,6 +40,13 @@ impl TeamMode { "Git".to_string(), "ControlHub".to_string(), "GetFileDiff".to_string(), + "SessionControl".to_string(), + "SessionMessage".to_string(), + "SessionHistory".to_string(), + "get_goal".to_string(), + "create_goal".to_string(), + "update_goal".to_string(), + "LegionControl".to_string(), ], } } diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/subagents/acp_agent.rs b/src/crates/assembly/core/src/agentic/agents/definitions/subagents/acp_agent.rs new file mode 100644 index 000000000..5203ed90a --- /dev/null +++ b/src/crates/assembly/core/src/agentic/agents/definitions/subagents/acp_agent.rs @@ -0,0 +1,77 @@ +//! ACP bridge agent — an AgentRegistry entry for every configured ACP client. +//! +//! Each ACP client (OpenCode, Claude Code, CodeBuddy, etc.) is represented as a +//! `SubAgent` so it appears in the agent selector and can be targeted by +//! `SessionControl` / `SessionMessage` for legion orchestration. + +use crate::agentic::agents::{Agent, UserContextPolicy}; +use async_trait::async_trait; + +/// A thin Agent wrapper around a single ACP client config. +#[allow(dead_code)] +pub struct AcpAgent { + agent_id: String, + display_name: String, + default_tools: Vec, +} + +impl AcpAgent { + pub fn new(client_id: String, display_name: String) -> Self { + let agent_id = Self::agent_id_for(&client_id); + Self { + // ACP prompt tool is registered in the global tool registry by + // register_configured_tools() — do NOT add it to default_tools + // here, or the tool name will appear twice in the model manifest. + default_tools: vec![ + "Read".to_string(), + "Grep".to_string(), + "Glob".to_string(), + "LS".to_string(), + ], + agent_id, + display_name, + } + } + + /// The agent registry id: `acp__` + pub fn agent_id_for(client_id: &str) -> String { + format!("acp__{client_id}") + } +} + +#[async_trait] +impl Agent for AcpAgent { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn id(&self) -> &str { + &self.agent_id + } + + fn name(&self) -> &str { + &self.display_name + } + + fn description(&self) -> &str { + "ACP agent" + } + + fn prompt_template_name(&self, _model_name: Option<&str>) -> &str { + "acp_agent" + } + + fn default_tools(&self) -> Vec { + self.default_tools.clone() + } + + fn user_context_policy(&self) -> UserContextPolicy { + UserContextPolicy::empty() + .with_workspace_context() + .with_workspace_instructions() + } + + fn is_readonly(&self) -> bool { + false + } +} diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/subagents/mod.rs b/src/crates/assembly/core/src/agentic/agents/definitions/subagents/mod.rs index 37309c8e3..3b1828495 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/subagents/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/subagents/mod.rs @@ -1,9 +1,11 @@ +mod acp_agent; mod computer_use; mod explore; mod file_finder; mod general_purpose; mod research_specialist; +pub use acp_agent::AcpAgent; pub use computer_use::ComputerUseMode; pub use explore::ExploreAgent; pub use file_finder::FileFinderAgent; diff --git a/src/crates/assembly/core/src/agentic/agents/mod.rs b/src/crates/assembly/core/src/agentic/agents/mod.rs index ca52676f5..db918ed50 100644 --- a/src/crates/assembly/core/src/agentic/agents/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/mod.rs @@ -35,7 +35,8 @@ pub use definitions::review::{ }; pub use definitions::shared::ReadonlySubagent; pub use definitions::subagents::{ - ComputerUseMode, ExploreAgent, FileFinderAgent, GeneralPurposeAgent, ResearchSpecialistAgent, + AcpAgent, ComputerUseMode, ExploreAgent, FileFinderAgent, GeneralPurposeAgent, + ResearchSpecialistAgent, }; use indexmap::IndexMap; pub use prompt_builder::{ @@ -132,6 +133,8 @@ pub fn shared_coding_mode_tools() -> Vec { "ReviewPlatform".to_string(), "ControlHub".to_string(), "InitMiniApp".to_string(), + "SessionMessage".to_string(), + "SessionHistory".to_string(), ]; append_provider_group_tools(&mut tools, "core.canvas"); tools diff --git a/src/crates/assembly/core/src/agentic/agents/prompts/acp_agent.md b/src/crates/assembly/core/src/agentic/agents/prompts/acp_agent.md new file mode 100644 index 000000000..08d0629d7 --- /dev/null +++ b/src/crates/assembly/core/src/agentic/agents/prompts/acp_agent.md @@ -0,0 +1,17 @@ +You are a bridge to an external ACP agent running inside BitFun. A commander has delegated a task to you. Your job is to forward the task through your ACP tool and return the result, nothing more. + +## How You Work + +1. Read the task that was sent to you via SessionMessage. +2. Call your ACP prompt tool with the task as the `prompt` parameter. +3. Return the ACP agent's response exactly as received — do not summarise, reinterpret, or embellish. +4. If the ACP tool returns an error, report the error with the original task context so the commander can decide how to proceed. + +## Constraints + +- You do NOT have file write or edit capabilities by default. Your only execution tool is the ACP bridge. +- Do NOT ask the user questions. The commander is your only audience. +- Be concise. The commander is managing many agents and needs clear, direct responses. +- Do NOT pretend to perform work that should be delegated through the ACP tool. + +{LANGUAGE_PREFERENCE} diff --git a/src/crates/assembly/core/src/agentic/agents/registry/mod.rs b/src/crates/assembly/core/src/agentic/agents/registry/mod.rs index 0820c567f..30bea4d86 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/mod.rs @@ -127,6 +127,11 @@ impl AgentRegistry { .and_then(|entries| entries.get(agent_type).cloned()) } + /// Remove an agent entry by id (e.g. when an ACP client is disabled/removed). + pub fn unregister_agent(&self, id: &str) { + self.write_agents().remove(id); + } + /// Get a agent by ID (searches all categories including hidden) pub fn get_agent( &self, diff --git a/src/crates/assembly/core/src/agentic/agents/registry/query.rs b/src/crates/assembly/core/src/agentic/agents/registry/query.rs index 0597e576f..18f84e964 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/query.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/query.rs @@ -165,6 +165,23 @@ impl AgentRegistry { result } + /// Return ids of all agents visible for session creation (modes + subagents). + pub async fn get_agent_ids_for_session_creation(&self) -> Vec { + self.ensure_user_custom_agents_loaded().await; + let map = self.read_agents(); + let mut ids: Vec = map + .values() + .filter(|e| { + matches!(e.category, AgentCategory::Mode | AgentCategory::SubAgent) + }) + .map(|e| e.agent.id().to_string()) + .collect(); + drop(map); + ids.sort(); + ids.dedup(); + ids + } + /// check if a subagent is readonly (used for TaskTool.is_concurrency_safe etc.) pub fn get_subagent_is_readonly(&self, id: &str) -> Option { if let Some(entry) = self.read_agents().get(id) { diff --git a/src/crates/execution/agent-runtime/src/session_control.rs b/src/crates/execution/agent-runtime/src/session_control.rs index bf0e4e44b..532d518b9 100644 --- a/src/crates/execution/agent-runtime/src/session_control.rs +++ b/src/crates/execution/agent-runtime/src/session_control.rs @@ -24,23 +24,12 @@ impl SessionControlAction { } } -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -pub enum SessionControlAgentType { - #[serde(rename = "agentic", alias = "Agentic", alias = "AGENTIC")] - Agentic, - #[serde(rename = "Plan", alias = "plan", alias = "PLAN")] - Plan, - #[serde(rename = "Cowork", alias = "cowork", alias = "COWORK")] - Cowork, -} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionControlAgentType(pub String); impl SessionControlAgentType { - pub const fn as_str(&self) -> &'static str { - match self { - Self::Agentic => "agentic", - Self::Plan => "Plan", - Self::Cowork => "Cowork", - } + pub fn as_str(&self) -> &str { + &self.0 } } diff --git a/src/crates/execution/agent-runtime/tests/session_control_contracts.rs b/src/crates/execution/agent-runtime/tests/session_control_contracts.rs index 57ebaf23b..bdb698f99 100644 --- a/src/crates/execution/agent-runtime/tests/session_control_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/session_control_contracts.rs @@ -51,7 +51,7 @@ fn rejects_current_session_mutation_when_context_matches() { fn validates_create_requires_workspace_and_creator_session() { let mut input = base_input(SessionControlAction::Create); input.workspace = Some(std::env::temp_dir().to_string_lossy().to_string()); - input.agent_type = Some(SessionControlAgentType::Plan); + input.agent_type = Some(SessionControlAgentType("Plan".to_string())); let missing_creator = validate_session_control_input(&input, SessionControlValidationContext::default()); diff --git a/src/crates/interfaces/acp/src/client/manager.rs b/src/crates/interfaces/acp/src/client/manager.rs index 169e2258b..3764fa141 100644 --- a/src/crates/interfaces/acp/src/client/manager.rs +++ b/src/crates/interfaces/acp/src/client/manager.rs @@ -1507,6 +1507,34 @@ impl AcpClientService { debug!("Registering ACP client tool: name={}", tool.name()); registry.register_tool(tool); } + drop(registry); + + // Also register each ACP client as a SubAgent in the global AgentRegistry + // so they appear in the agent selector and can be targeted by + // SessionControl / SessionMessage for legion orchestration. + let agent_registry = + bitfun_core::agentic::agents::get_agent_registry(); + for (client_id, config) in configs.iter().filter(|(_, c)| c.enabled) { + let agent_id = + bitfun_core::agentic::agents::AcpAgent::agent_id_for( + client_id, + ); + // Clean up any previous registration for this client + agent_registry.unregister_agent(&agent_id); + let agent = Arc::new( + bitfun_core::agentic::agents::AcpAgent::new( + client_id.clone(), + config.name.clone().unwrap_or_else(|| client_id.clone()), + ), + ); + agent_registry.register_agent( + agent, + bitfun_core::agentic::agents::AgentCategory::Mode, + bitfun_core::agentic::agents::AgentSource::Builtin, + None, + None, + ); + } } async fn handle_permission_request(