Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
],
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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__<client_id>`
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<String> {
self.default_tools.clone()
}

fn user_context_policy(&self) -> UserContextPolicy {
UserContextPolicy::empty()
.with_workspace_context()
.with_workspace_instructions()
}

fn is_readonly(&self) -> bool {
false
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/crates/assembly/core/src/agentic/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -132,6 +133,8 @@ pub fn shared_coding_mode_tools() -> Vec<String> {
"ReviewPlatform".to_string(),
"ControlHub".to_string(),
"InitMiniApp".to_string(),
"SessionMessage".to_string(),
"SessionHistory".to_string(),
];
append_provider_group_tools(&mut tools, "core.canvas");
tools
Expand Down
17 changes: 17 additions & 0 deletions src/crates/assembly/core/src/agentic/agents/prompts/acp_agent.md
Original file line number Diff line number Diff line change
@@ -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}
5 changes: 5 additions & 0 deletions src/crates/assembly/core/src/agentic/agents/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/crates/assembly/core/src/agentic/agents/registry/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
self.ensure_user_custom_agents_loaded().await;
let map = self.read_agents();
let mut ids: Vec<String> = 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<bool> {
if let Some(entry) = self.read_agents().get(id) {
Expand Down
19 changes: 4 additions & 15 deletions src/crates/execution/agent-runtime/src/session_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
28 changes: 28 additions & 0 deletions src/crates/interfaces/acp/src/client/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down