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
10 changes: 9 additions & 1 deletion crates/forge_app/src/agent_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ impl<S: Services> AgentExecutor<S> {
&self,
agent_id: AgentId,
task: String,
cwd_override: Option<std::path::PathBuf>,
ctx: &ToolCallContext,
) -> anyhow::Result<ToolOutput> {
ctx.send_tool_input(
Expand All @@ -55,9 +56,16 @@ impl<S: Services> AgentExecutor<S> {
// Create a new conversation for agent execution
// Create context with agent initiator since it's spawned by a parent agent
let context = forge_domain::Context::default().initiator("agent".to_string());
let conversation = Conversation::generate()
let mut conversation = Conversation::generate()
.title(task.clone())
.context(context.clone());

// Set CWD override on the conversation so ForgeApp::chat uses it
// for environment, file listing, and extensions
if let Some(cwd) = cwd_override {
conversation = conversation.cwd(cwd);
}

self.services
.conversation_service()
.upsert_conversation(conversation.clone())
Expand Down
13 changes: 11 additions & 2 deletions crates/forge_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,18 @@ impl<S: Services> ForgeApp<S> {

// Discover files using the discovery service
let forge_config = services.get_config();
let environment = services.get_environment();
let cwd_override = conversation.cwd.clone();
let mut environment = services.get_environment();

let files = services.list_current_directory().await?;
// Apply CWD override from conversation (set by sub-agent invocations)
if let Some(ref cwd) = cwd_override {
environment.cwd = cwd.clone();
}

let files = match cwd_override {
Some(ref cwd) => services.list_directory_at(cwd.as_path()).await?,
None => services.list_current_directory().await?,
};

let custom_instructions = services.get_custom_instructions().await;

Expand Down
4 changes: 4 additions & 0 deletions crates/forge_app/src/command_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ mod tests {

Ok(files)
}

async fn list_directory_at(&self, _path: &std::path::Path) -> Result<Vec<File>> {
self.list_current_directory().await
}
}

#[async_trait::async_trait]
Expand Down
1 change: 1 addition & 0 deletions crates/forge_app/src/hooks/doom_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ mod tests {
context: Some(context),
metrics: Default::default(),
metadata: forge_domain::MetaData::new(chrono::Utc::now()),
cwd: None,
}
}

Expand Down
8 changes: 8 additions & 0 deletions crates/forge_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ pub trait FileDiscoveryService: Send + Sync {
/// Lists all entries (files and directories) in the current directory
/// Returns a sorted vector of File entries with directories first
async fn list_current_directory(&self) -> anyhow::Result<Vec<File>>;

/// Lists all entries (files and directories) at the specified path
/// Returns a sorted vector of File entries with directories first
async fn list_directory_at(&self, path: &std::path::Path) -> anyhow::Result<Vec<File>>;
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -747,6 +751,10 @@ impl<I: Services> FileDiscoveryService for I {
async fn list_current_directory(&self) -> anyhow::Result<Vec<File>> {
self.file_discovery_service().list_current_directory().await
}

async fn list_directory_at(&self, path: &std::path::Path) -> anyhow::Result<Vec<File>> {
self.file_discovery_service().list_directory_at(path).await
}
}

#[async_trait::async_trait]
Expand Down
4 changes: 3 additions & 1 deletion crates/forge_app/src/tool_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,13 @@ impl<S: Services> ToolRegistry<S> {
} else if self.agent_executor.contains_tool(&input.name).await? {
// Handle agent delegation tool calls
let agent_input = AgentInput::try_from(&input)?;
let cwd_override = agent_input.cwd.map(std::path::PathBuf::from);
let executor = self.agent_executor.clone();
// NOTE: Agents should not timeout
let outputs =
join_all(agent_input.tasks.into_iter().map(|task| {
executor.execute(AgentId::new(input.name.as_str()), task, context)
let cwd = cwd_override.clone();
executor.execute(AgentId::new(input.name.as_str()), task, cwd, context)
}))
.await
.into_iter()
Expand Down
6 changes: 6 additions & 0 deletions crates/forge_domain/src/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ pub struct Conversation {
pub context: Option<Context>,
pub metrics: Metrics,
pub metadata: MetaData,
/// Optional working directory override for sub-agent conversations.
/// When set, the agent uses this path as its CWD instead of the parent
/// process's CWD for system information (file listing, extensions, env.cwd).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<std::path::PathBuf>,
}

#[derive(Debug, Setters, Serialize, Deserialize, Clone)]
Expand All @@ -71,6 +76,7 @@ impl Conversation {
metadata: MetaData::new(created_at),
title: None,
context: None,
cwd: None,
}
}
/// Creates a new conversation with a new conversation ID.
Expand Down
6 changes: 6 additions & 0 deletions crates/forge_domain/src/tools/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ pub struct AgentInput {
/// requirements to enable the agent to understand and execute the work
/// accurately.
pub tasks: Vec<String>,
/// Optional working directory override for the agent. When provided, the
/// agent will treat this path as its current working directory instead of
/// the parent process's CWD. This affects the system information shown to
/// the agent (file listing, extensions, env.cwd).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
}

fn default_true() -> bool {
Expand Down
1 change: 1 addition & 0 deletions crates/forge_main/src/conversation_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ mod tests {
context: None,
metrics: Metrics::default().started_at(now),
metadata: MetaData { created_at: now, updated_at: Some(now) },
cwd: None,
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/forge_main/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ mod tests {
context: None,
metrics,
metadata: forge_domain::MetaData::new(Utc::now()),
cwd: None,
};

let actual = super::Info::from(&fixture);
Expand Down Expand Up @@ -1006,6 +1007,7 @@ mod tests {
context: None,
metrics,
metadata: forge_domain::MetaData::new(Utc::now()),
cwd: None,
};

let actual = super::Info::from(&fixture);
Expand Down Expand Up @@ -1051,6 +1053,7 @@ mod tests {
context: Some(context),
metrics,
metadata: forge_domain::MetaData::new(Utc::now()),
cwd: None,
};

let actual = super::Info::from(&fixture);
Expand Down
6 changes: 5 additions & 1 deletion crates/forge_services/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ impl<F: EnvironmentInfra + WalkerInfra + DirectoryReaderInfra + Send + Sync> Fil

async fn list_current_directory(&self) -> Result<Vec<File>> {
let env = self.service.get_environment();
let entries = self.service.list_directory_entries(&env.cwd).await?;
self.list_directory_at(&env.cwd).await
}

async fn list_directory_at(&self, path: &std::path::Path) -> Result<Vec<File>> {
let entries = self.service.list_directory_entries(path).await?;

let mut files: Vec<File> = entries
.into_iter()
Expand Down
Loading