Skip to content

Commit b83088d

Browse files
ssfdustforge-code-agentDeepSeek
committed
feat(forge): add tool_silent mode propagation for headless/ACP execution
Add a `tool_silent` flag to `ChatRequest` that propagates through the entire tool execution chain down to the command executor. When `true`, shell tool output is suppressed on stdout to protect JSON-RPC transports. - chat_request: add `tool_silent: bool` field (default false) - orch: add `tool_silent` field, propagate to ToolCallContext - app: forward ChatRequest.tool_silent to Orchestrator - agent_executor: inherit silent flag from parent ToolCallContext - executor: clean up TODO(acp), add inline comment Signed-off-by: ssfdust <ssfdust@gmail.com> Co-Authored-By: ForgeCode <noreply@forgecode.dev> Co-Authored-By: DeepSeek <contact@deepseek.com>
1 parent bd98050 commit b83088d

5 files changed

Lines changed: 24 additions & 16 deletions

File tree

crates/forge_app/src/agent_executor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> AgentEx
7575
.await?;
7676
conversation
7777
};
78-
// Execute the request through the ForgeApp
78+
// Execute the request through the ForgeApp, propagating silent mode.
7979
let app = crate::ForgeApp::new(self.services.clone());
8080
let mut response_stream = app
8181
.chat(
8282
agent_id.clone(),
83-
ChatRequest::new(Event::new(task.clone()), conversation.id),
83+
ChatRequest::new(Event::new(task.clone()), conversation.id).tool_silent(ctx.silent),
8484
)
8585
.await?;
8686

crates/forge_app/src/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> ForgeAp
180180
.error_tracker(ToolErrorTracker::new(max_tool_failure_per_turn))
181181
.tool_definitions(tool_definitions)
182182
.models(models)
183-
.hook(Arc::new(hook));
183+
.hook(Arc::new(hook))
184+
.tool_silent(chat.tool_silent);
184185

185186
// Create and return the stream
186187
let stream = MpscStream::spawn(

crates/forge_app/src/orch.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ pub struct Orchestrator<S> {
2626
error_tracker: ToolErrorTracker,
2727
hook: Arc<Hook>,
2828
config: forge_config::ForgeConfig,
29+
/// When `true`, shell tool output is suppressed on stdout to avoid
30+
/// contaminating the ACP JSON-RPC transport.
31+
///
32+
/// Set from [`ChatRequest::tool_silent`] during `ForgeApp::chat()`. This field
33+
/// is consumed in [`Orchestrator::run()`] when constructing the
34+
/// [`ToolCallContext`].
35+
tool_silent: bool,
2936
}
3037

3138
impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orchestrator<S> {
@@ -45,6 +52,7 @@ impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orc
4552
models: Default::default(),
4653
error_tracker: Default::default(),
4754
hook: Arc::new(Hook::default()),
55+
tool_silent: false,
4856
}
4957
}
5058

@@ -262,11 +270,11 @@ impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orc
262270

263271
// Retrieve the number of requests allowed per tick.
264272
let max_requests_per_turn = self.agent.max_requests_per_turn;
265-
// TODO(acp): when running in ACP stdio mode, chain `.silent(true)`
266-
// so that shell tool output is suppressed on stdout (see
267-
// ToolCallContext and tool_executor).
268-
let tool_context =
269-
ToolCallContext::new(self.conversation.metrics.clone()).sender(self.sender.clone());
273+
// Propagate silent mode to ToolCallContext.
274+
// See designs/acp-silent-mode-propagation.md.
275+
let tool_context = ToolCallContext::new(self.conversation.metrics.clone())
276+
.sender(self.sender.clone())
277+
.silent(self.tool_silent);
270278

271279
while !should_yield {
272280
// Set context for the current loop iteration

crates/forge_domain/src/chat_request.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ use crate::{ConversationId, Event};
88
pub struct ChatRequest {
99
pub event: Event,
1010
pub conversation_id: ConversationId,
11+
/// When `true`, shell tool output is suppressed on stdout (routed to
12+
/// `io::sink()`) to protect the ACP JSON-RPC transport.
13+
/// See `designs/acp-silent-mode-propagation.md`.
14+
pub tool_silent: bool,
1115
}
1216

1317
impl ChatRequest {
1418
pub fn new(content: Event, conversation_id: ConversationId) -> Self {
15-
Self { event: content, conversation_id }
19+
Self { event: content, conversation_id, tool_silent: false }
1620
}
1721
}

crates/forge_infra/src/executor.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,8 @@ impl ForgeCommandExecutorService {
108108
let mut stdout_pipe = child.stdout.take();
109109
let mut stderr_pipe = child.stderr.take();
110110

111-
// Stream the output of the command to stdout and stderr concurrently.
112-
//
113-
// TODO(acp): when `silent` is true (ACP stdio mode), output is
114-
// streamed to io::sink() so that raw command output does not
115-
// contaminate the ACP JSON-RPC transport on stdout. The captured
116-
// output is still returned via CommandOutput for the ACP
117-
// notification channel.
111+
// Suppress stdout in headless mode to avoid contaminating the JSON-RPC
112+
// transport.
118113
let (status, stdout_buffer, stderr_buffer) = if silent {
119114
tokio::try_join!(
120115
child.wait(),

0 commit comments

Comments
 (0)