Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/apps/cli/src/acp/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ async fn handle_tools_call(
custom_data: std::collections::HashMap::new(),
computer_use_host: None,
cancellation_token: None,
runtime_tool_restrictions: Default::default(),
workspace_services: None,
};

Expand Down Expand Up @@ -678,4 +679,4 @@ fn handle_set_config_option(_request: &JsonRpcRequest) -> Result<serde_json::Val
fn handle_set_mode(_request: &JsonRpcRequest) -> Result<serde_json::Value> {
// TODO: Implement mode switching
Ok(serde_json::json!({ "success": true }))
}
}
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/tool_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ fn build_tool_context(workspace_path: Option<&str>) -> ToolUseContext {
custom_data: HashMap::new(),
computer_use_host: None,
cancellation_token: None,
runtime_tool_restrictions: Default::default(),
workspace_services: None,
}
}
Expand Down
227 changes: 184 additions & 43 deletions src/crates/core/src/agentic/coordination/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ use crate::agentic::events::{
AgenticEvent, EventPriority, EventQueue, EventRouter, EventSubscriber,
};
use crate::agentic::execution::{ContextCompactionOutcome, ExecutionContext, ExecutionEngine};
use crate::agentic::fork::{ForkContextSnapshot, ForkExecutionRequest, ForkExecutionResult};
use crate::agentic::image_analysis::ImageContextData;
use crate::agentic::round_preempt::DialogRoundPreemptSource;
use crate::agentic::session::SessionManager;
use crate::agentic::tools::ToolRuntimeRestrictions;
use crate::agentic::tools::pipeline::{SubagentParentInfo, ToolPipeline};
use crate::agentic::WorkspaceBinding;
use crate::service::bootstrap::{
ensure_workspace_persona_files_for_prompt, is_workspace_bootstrap_pending,
};
use crate::util::errors::{BitFunError, BitFunResult};
use log::{debug, error, info, warn};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::OnceLock;
Expand All @@ -41,6 +44,17 @@ pub struct SubagentResult {
pub text: String,
}

struct HiddenSubagentExecutionRequest {
session_name: String,
agent_type: String,
session_config: SessionConfig,
initial_messages: Vec<Message>,
created_by: Option<String>,
subagent_parent_info: Option<SubagentParentInfo>,
context: HashMap<String, String>,
runtime_tool_restrictions: ToolRuntimeRestrictions,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DialogTriggerSource {
DesktopUi,
Expand Down Expand Up @@ -683,29 +697,62 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
}
}

/// Create a subagent session for internal AI execution.
/// Create a hidden subagent session for internal AI execution.
/// Unlike `create_session`, this does NOT emit `SessionCreated` to the transport layer,
/// because subagent sessions are internal implementation details of the execution engine
/// because hidden child sessions are internal implementation details of the execution engine
/// and must never appear as top-level items in the UI.
async fn create_subagent_session(
async fn create_hidden_subagent_session(
&self,
session_name: String,
agent_type: String,
config: SessionConfig,
parent_info: &SubagentParentInfo,
created_by: Option<String>,
) -> BitFunResult<Session> {
self.session_manager
.create_session_with_id_and_details(
None,
session_name,
agent_type,
config,
Some(format!("session-{}", parent_info.session_id)),
created_by,
SessionKind::Subagent,
)
.await
}

async fn load_session_context_messages(&self, session: &Session) -> BitFunResult<Vec<Message>> {
let session_id = &session.session_id;
let mut context_messages = self
.session_manager
.get_context_messages(session_id)
.await?;

if context_messages.is_empty() && !session.dialog_turn_ids.is_empty() {
if let Some(workspace_path) = session.config.workspace_path.as_deref() {
match self
.session_manager
.restore_session(Path::new(workspace_path), session_id)
.await
{
Ok(_) => {
context_messages = self
.session_manager
.get_context_messages(session_id)
.await?;
}
Err(e) => {
debug!(
"Failed to restore parent session context for fork capture: session_id={}, error={}",
session_id, e
);
}
}
}
}

Ok(context_messages)
}

async fn wrap_user_input(
&self,
agent_type: &str,
Expand Down Expand Up @@ -1438,6 +1485,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
context: context_vars,
subagent_parent_info: None,
skip_tool_confirmation: submission_policy.skip_tool_confirmation,
runtime_tool_restrictions: ToolRuntimeRestrictions::default(),
workspace_services,
round_preempt: self.round_preempt_source.get().cloned(),
};
Expand Down Expand Up @@ -1871,24 +1919,9 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
self.tool_pipeline.cancel_tool(tool_id, reason).await
}

/// Execute subagent task directly
/// DialogTurnStarted event not needed for now
///
/// Parameters:
/// - agent_type: Agent type
/// - task_description: Task description
/// - subagent_parent_info: Parent info (tool call context)
/// - context: Additional context
/// - cancel_token: Optional cancel token (for async cancellation)
///
/// Returns SubagentResult with the final text response
pub async fn execute_subagent(
async fn execute_hidden_subagent_internal(
&self,
agent_type: String,
task_description: String,
subagent_parent_info: SubagentParentInfo,
workspace_path: Option<String>,
context: Option<std::collections::HashMap<String, String>>,
request: HiddenSubagentExecutionRequest,
cancel_token: Option<&CancellationToken>,
) -> BitFunResult<SubagentResult> {
// Check cancel token (before creating session)
Expand All @@ -1901,25 +1934,23 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
}
}

// Create independent subagent session.
// Use create_subagent_session (not create_session) so that no SessionCreated
// event is emitted to the transport layer — subagent sessions are internal
// implementation details and must not appear in the UI session list.
let workspace_path = workspace_path.ok_or_else(|| {
BitFunError::Validation(
"workspace_path is required when creating a subagent session".to_string(),
)
})?;
let subagent_config = SessionConfig {
workspace_path: Some(workspace_path),
..SessionConfig::default()
};
let HiddenSubagentExecutionRequest {
session_name,
agent_type,
session_config,
initial_messages,
created_by,
subagent_parent_info,
context,
runtime_tool_restrictions,
} = request;

let session = self
.create_subagent_session(
format!("Subagent: {}", task_description),
.create_hidden_subagent_session(
session_name,
agent_type.clone(),
subagent_config,
&subagent_parent_info,
session_config,
created_by,
)
.await?;

Expand Down Expand Up @@ -1973,18 +2004,17 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
turn_index: 0,
agent_type: agent_type.clone(),
workspace: subagent_workspace,
context: context.unwrap_or_default(),
subagent_parent_info: Some(subagent_parent_info),
context,
subagent_parent_info: subagent_parent_info.clone(),
// Subagents run autonomously without user interaction; always skip
// tool confirmation to prevent them from blocking indefinitely on a
// confirmation channel that nobody will ever respond to.
skip_tool_confirmation: true,
runtime_tool_restrictions,
workspace_services: subagent_services,
round_preempt: self.round_preempt_source.get().cloned(),
};

let initial_messages = vec![Message::user(task_description)];

let result = self
.execution_engine
.execute_dialog_turn(agent_type, initial_messages, execution_context)
Expand Down Expand Up @@ -2039,6 +2069,117 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
})
}

pub async fn capture_fork_context_snapshot(
&self,
parent_session_id: &str,
) -> BitFunResult<ForkContextSnapshot> {
let parent_session = self
.session_manager
.get_session(parent_session_id)
.ok_or_else(|| {
BitFunError::NotFound(format!("Parent session not found: {}", parent_session_id))
})?;
let context_messages = self.load_session_context_messages(&parent_session).await?;
ForkContextSnapshot::from_parent_session(&parent_session, context_messages)
}

/// Execute a hidden child agent that inherits the parent session's current
/// model-visible context.
pub async fn execute_forked_agent(
&self,
request: ForkExecutionRequest,
cancel_token: Option<&CancellationToken>,
) -> BitFunResult<ForkExecutionResult> {
if request.agent_type.trim().is_empty() {
return Err(BitFunError::Validation(
"ForkExecutionRequest.agent_type is required".to_string(),
));
}
if request.description.trim().is_empty() {
return Err(BitFunError::Validation(
"ForkExecutionRequest.description is required".to_string(),
));
}
if request.prompt_messages.is_empty() {
return Err(BitFunError::Validation(
"ForkExecutionRequest.prompt_messages must not be empty".to_string(),
));
}

let inherited_message_count = request.snapshot.inherited_message_count();
let prompt_message_count = request.prompt_messages.len();
let agent_type = request.agent_type.clone();
let session_config = request.child_session_config();
let initial_messages = request.composed_initial_messages();
let created_by = Some(format!("session-{}", request.snapshot.parent_session_id));
let child_result = self
.execute_hidden_subagent_internal(
HiddenSubagentExecutionRequest {
session_name: format!("Fork: {}", request.description),
agent_type,
session_config,
initial_messages,
created_by,
subagent_parent_info: None,
context: request.context,
runtime_tool_restrictions: request.runtime_tool_restrictions,
},
cancel_token,
)
.await?;

Ok(ForkExecutionResult {
text: child_result.text,
inherited_message_count,
prompt_message_count,
})
}

/// Execute subagent task directly
/// DialogTurnStarted event not needed for now
///
/// Parameters:
/// - agent_type: Agent type
/// - task_description: Task description
/// - subagent_parent_info: Parent info (tool call context)
/// - context: Additional context
/// - cancel_token: Optional cancel token (for async cancellation)
///
/// Returns SubagentResult with the final text response
pub async fn execute_subagent(
&self,
agent_type: String,
task_description: String,
subagent_parent_info: SubagentParentInfo,
workspace_path: Option<String>,
context: Option<HashMap<String, String>>,
cancel_token: Option<&CancellationToken>,
) -> BitFunResult<SubagentResult> {
let workspace_path = workspace_path.ok_or_else(|| {
BitFunError::Validation(
"workspace_path is required when creating a subagent session".to_string(),
)
})?;

self.execute_hidden_subagent_internal(
HiddenSubagentExecutionRequest {
session_name: format!("Subagent: {}", task_description),
agent_type,
session_config: SessionConfig {
workspace_path: Some(workspace_path),
..SessionConfig::default()
},
initial_messages: vec![Message::user(task_description)],
created_by: Some(format!("session-{}", subagent_parent_info.session_id)),
subagent_parent_info: Some(subagent_parent_info),
context: context.unwrap_or_default(),
runtime_tool_restrictions: ToolRuntimeRestrictions::default(),
},
cancel_token,
)
.await
}

/// Clean up subagent session resources
///
/// Release resources occupied by subagent session (sandbox, etc.) and delete session
Expand Down
6 changes: 5 additions & 1 deletion src/crates/core/src/agentic/execution/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ use crate::agentic::image_analysis::{
ImageLimits,
};
use crate::agentic::session::{CompressionTailPolicy, ContextCompressor, SessionManager};
use crate::agentic::tools::{get_all_registered_tools, SubagentParentInfo};
use crate::agentic::tools::{
get_all_registered_tools, SubagentParentInfo, ToolRuntimeRestrictions,
};
use crate::agentic::util::build_remote_workspace_layout_preview;
use crate::agentic::{WorkspaceBackend, WorkspaceBinding};
use crate::infrastructure::ai::get_global_ai_client_factory;
Expand Down Expand Up @@ -1432,6 +1434,7 @@ impl ExecutionEngine {
model_name: ai_client.config.model.clone(),
agent_type: agent_type.clone(),
context_vars: round_context_vars,
runtime_tool_restrictions: context.runtime_tool_restrictions.clone(),
cancellation_token: CancellationToken::new(),
workspace_services: context.workspace_services.clone(),
};
Expand Down Expand Up @@ -1702,6 +1705,7 @@ impl ExecutionEngine {
custom_data: tool_opts_custom,
computer_use_host: None,
cancellation_token: None,
runtime_tool_restrictions: ToolRuntimeRestrictions::default(),
workspace_services: None,
};
for tool in &all_tools {
Expand Down
1 change: 1 addition & 0 deletions src/crates/core/src/agentic/execution/round_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ impl RoundExecutor {
context_vars: context.context_vars.clone(),
subagent_parent_info,
allowed_tools: context.available_tools.clone(),
runtime_tool_restrictions: context.runtime_tool_restrictions.clone(),
workspace_services: context.workspace_services.clone(),
};

Expand Down
Loading
Loading