Approvals for ACP-based executors (#1511)
* Approvals for ACP-based executors Gemini, Qwen, and Opencode * set all permissions to "ask" * use `normalize_unified_diff` in other log normalizers
This commit is contained in:
@@ -46,6 +46,11 @@
|
|||||||
"model": "gemini-3-pro-preview",
|
"model": "gemini-3-pro-preview",
|
||||||
"yolo": true
|
"yolo": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"APPROVALS": {
|
||||||
|
"GEMINI": {
|
||||||
|
"yolo": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CODEX": {
|
"CODEX": {
|
||||||
@@ -78,11 +83,19 @@
|
|||||||
},
|
},
|
||||||
"OPENCODE": {
|
"OPENCODE": {
|
||||||
"DEFAULT": {
|
"DEFAULT": {
|
||||||
"OPENCODE": {}
|
"OPENCODE": {
|
||||||
|
"auto_approve": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"PLAN": {
|
"PLAN": {
|
||||||
"OPENCODE": {
|
"OPENCODE": {
|
||||||
"mode": "plan"
|
"mode": "plan",
|
||||||
|
"auto_approve": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"APPROVALS": {
|
||||||
|
"OPENCODE": {
|
||||||
|
"auto_approve": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,6 +104,11 @@
|
|||||||
"QWEN_CODE": {
|
"QWEN_CODE": {
|
||||||
"yolo": true
|
"yolo": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"APPROVALS": {
|
||||||
|
"QWEN_CODE": {
|
||||||
|
"yolo": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CURSOR_AGENT": {
|
"CURSOR_AGENT": {
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ impl ExecutionEnv {
|
|||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn contains_key(&self, key: &str) -> bool {
|
||||||
|
self.vars.contains_key(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
use agent_client_protocol as acp;
|
use std::sync::Arc;
|
||||||
use async_trait::async_trait;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tracing::{debug, warn};
|
|
||||||
|
|
||||||
use crate::executors::acp::AcpEvent;
|
use agent_client_protocol::{self as acp, ErrorCode};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
use workspace_utils::approvals::ApprovalStatus;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
approvals::{ExecutorApprovalError, ExecutorApprovalService},
|
||||||
|
executors::acp::{AcpEvent, ApprovalResponse},
|
||||||
|
};
|
||||||
|
|
||||||
/// ACP client that handles agent-client protocol communication
|
/// ACP client that handles agent-client protocol communication
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct AcpClient {
|
pub struct AcpClient {
|
||||||
event_tx: mpsc::UnboundedSender<AcpEvent>,
|
event_tx: mpsc::UnboundedSender<AcpEvent>,
|
||||||
|
approvals: Option<Arc<dyn ExecutorApprovalService>>,
|
||||||
|
feedback_queue: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcpClient {
|
impl AcpClient {
|
||||||
/// Create a new ACP client
|
/// Create a new ACP client
|
||||||
pub fn new(event_tx: mpsc::UnboundedSender<AcpEvent>) -> Self {
|
pub fn new(
|
||||||
Self { event_tx }
|
event_tx: mpsc::UnboundedSender<AcpEvent>,
|
||||||
|
approvals: Option<Arc<dyn ExecutorApprovalService>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
event_tx,
|
||||||
|
approvals,
|
||||||
|
feedback_queue: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_user_prompt_event(&self, prompt: &str) {
|
pub fn record_user_prompt_event(&self, prompt: &str) {
|
||||||
@@ -26,6 +42,21 @@ impl AcpClient {
|
|||||||
warn!("Failed to send ACP event: {}", e);
|
warn!("Failed to send ACP event: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queue a user feedback message to be sent after a denial.
|
||||||
|
pub async fn enqueue_feedback(&self, message: String) {
|
||||||
|
let trimmed = message.trim().to_string();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
let mut q = self.feedback_queue.lock().await;
|
||||||
|
q.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain and return queued feedback messages.
|
||||||
|
pub async fn drain_feedback(&self) -> Vec<String> {
|
||||||
|
let mut q = self.feedback_queue.lock().await;
|
||||||
|
q.drain(..).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
@@ -34,31 +65,107 @@ impl acp::Client for AcpClient {
|
|||||||
&self,
|
&self,
|
||||||
args: acp::RequestPermissionRequest,
|
args: acp::RequestPermissionRequest,
|
||||||
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
||||||
// Forward the request as an event
|
|
||||||
self.send_event(AcpEvent::RequestPermission(args.clone()));
|
self.send_event(AcpEvent::RequestPermission(args.clone()));
|
||||||
|
|
||||||
// Auto-approve with best available option
|
if self.approvals.is_none() {
|
||||||
let chosen_option = args
|
// Auto-approve with best available option when no approval service is configured
|
||||||
.options
|
let chosen_option = args
|
||||||
.iter()
|
.options
|
||||||
.find(|o| matches!(o.kind, acp::PermissionOptionKind::AllowAlways))
|
.iter()
|
||||||
.or_else(|| {
|
.find(|o| matches!(o.kind, acp::PermissionOptionKind::AllowAlways))
|
||||||
args.options
|
.or_else(|| {
|
||||||
.iter()
|
args.options
|
||||||
.find(|o| matches!(o.kind, acp::PermissionOptionKind::AllowOnce))
|
.iter()
|
||||||
})
|
.find(|o| matches!(o.kind, acp::PermissionOptionKind::AllowOnce))
|
||||||
.or_else(|| args.options.first());
|
})
|
||||||
|
.or_else(|| args.options.first());
|
||||||
|
|
||||||
let outcome = if let Some(opt) = chosen_option {
|
let outcome = if let Some(opt) = chosen_option {
|
||||||
debug!("Auto-approving permission with option: {}", opt.option_id);
|
debug!("Auto-approving permission with option: {}", opt.option_id);
|
||||||
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
|
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
|
||||||
opt.option_id.clone(),
|
opt.option_id.clone(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
warn!("No permission options available, cancelling");
|
warn!("No permission options available, cancelling");
|
||||||
acp::RequestPermissionOutcome::Cancelled
|
acp::RequestPermissionOutcome::Cancelled
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(acp::RequestPermissionResponse::new(outcome));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tool_call_id = args.tool_call.tool_call_id.0.to_string();
|
||||||
|
let status = match self
|
||||||
|
.approvals
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ExecutorApprovalError::ServiceUnavailable)
|
||||||
|
.map_err(|_| acp::Error::invalid_request())?
|
||||||
|
.request_tool_approval(
|
||||||
|
args.tool_call.fields.title.as_deref().unwrap_or("tool"),
|
||||||
|
serde_json::json!({ "tool_call": args.tool_call }),
|
||||||
|
&tool_call_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to request tool approval: {}", err);
|
||||||
|
return Err(acp::Error::new(
|
||||||
|
ErrorCode::INTERNAL_ERROR.code,
|
||||||
|
format!("Approval request failed: {}", err),
|
||||||
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map our ApprovalStatus to ACP outcome
|
||||||
|
let outcome = match &status {
|
||||||
|
ApprovalStatus::Approved => {
|
||||||
|
let chosen = args
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.find(|o| matches!(o.kind, acp::PermissionOptionKind::AllowOnce));
|
||||||
|
if let Some(opt) = chosen {
|
||||||
|
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
|
||||||
|
opt.option_id.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
tracing::error!("No suitable approval option found, cancelling");
|
||||||
|
return Err(acp::Error::invalid_request());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApprovalStatus::Denied { reason } => {
|
||||||
|
// If user provided a reason, queue it to send after denial
|
||||||
|
if let Some(feedback) = reason.as_ref() {
|
||||||
|
self.enqueue_feedback(feedback.clone()).await;
|
||||||
|
}
|
||||||
|
let chosen = args
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.find(|o| matches!(o.kind, acp::PermissionOptionKind::RejectOnce));
|
||||||
|
if let Some(opt) = chosen {
|
||||||
|
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
|
||||||
|
opt.option_id.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
warn!("No permission options for denial, cancelling");
|
||||||
|
acp::RequestPermissionOutcome::Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApprovalStatus::TimedOut => {
|
||||||
|
warn!("Approval timed out");
|
||||||
|
acp::RequestPermissionOutcome::Cancelled
|
||||||
|
}
|
||||||
|
ApprovalStatus::Pending => {
|
||||||
|
// This should not occur after waiter resolves
|
||||||
|
warn!("Approval resolved to Pending");
|
||||||
|
acp::RequestPermissionOutcome::Cancelled
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_event(AcpEvent::ApprovalResponse(ApprovalResponse {
|
||||||
|
tool_call_id: tool_call_id.clone(),
|
||||||
|
status: status.clone(),
|
||||||
|
}));
|
||||||
|
|
||||||
Ok(acp::RequestPermissionResponse::new(outcome))
|
Ok(acp::RequestPermissionResponse::new(outcome))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,10 +15,11 @@ use tokio_util::{
|
|||||||
io::ReaderStream,
|
io::ReaderStream,
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use workspace_utils::stream_lines::LinesStreamExt;
|
use workspace_utils::{approvals::ApprovalStatus, stream_lines::LinesStreamExt};
|
||||||
|
|
||||||
use super::{AcpClient, SessionManager};
|
use super::{AcpClient, SessionManager};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
approvals::ExecutorApprovalService,
|
||||||
command::{CmdOverrides, CommandParts},
|
command::{CmdOverrides, CommandParts},
|
||||||
env::ExecutionEnv,
|
env::ExecutionEnv,
|
||||||
executors::{ExecutorError, ExecutorExitResult, SpawnedChild, acp::AcpEvent},
|
executors::{ExecutorError, ExecutorExitResult, SpawnedChild, acp::AcpEvent},
|
||||||
@@ -73,6 +75,7 @@ impl AcpAgentHarness {
|
|||||||
command_parts: CommandParts,
|
command_parts: CommandParts,
|
||||||
env: &ExecutionEnv,
|
env: &ExecutionEnv,
|
||||||
cmd_overrides: &CmdOverrides,
|
cmd_overrides: &CmdOverrides,
|
||||||
|
approvals: Option<std::sync::Arc<dyn ExecutorApprovalService>>,
|
||||||
) -> Result<SpawnedChild, ExecutorError> {
|
) -> Result<SpawnedChild, ExecutorError> {
|
||||||
let (program_path, args) = command_parts.into_resolved().await?;
|
let (program_path, args) = command_parts.into_resolved().await?;
|
||||||
let mut command = Command::new(program_path);
|
let mut command = Command::new(program_path);
|
||||||
@@ -101,6 +104,7 @@ impl AcpAgentHarness {
|
|||||||
self.session_namespace.clone(),
|
self.session_namespace.clone(),
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.mode.clone(),
|
self.mode.clone(),
|
||||||
|
approvals,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -111,6 +115,7 @@ impl AcpAgentHarness {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn spawn_follow_up_with_command(
|
pub async fn spawn_follow_up_with_command(
|
||||||
&self,
|
&self,
|
||||||
current_dir: &Path,
|
current_dir: &Path,
|
||||||
@@ -119,6 +124,7 @@ impl AcpAgentHarness {
|
|||||||
command_parts: CommandParts,
|
command_parts: CommandParts,
|
||||||
env: &ExecutionEnv,
|
env: &ExecutionEnv,
|
||||||
cmd_overrides: &CmdOverrides,
|
cmd_overrides: &CmdOverrides,
|
||||||
|
approvals: Option<std::sync::Arc<dyn ExecutorApprovalService>>,
|
||||||
) -> Result<SpawnedChild, ExecutorError> {
|
) -> Result<SpawnedChild, ExecutorError> {
|
||||||
let (program_path, args) = command_parts.into_resolved().await?;
|
let (program_path, args) = command_parts.into_resolved().await?;
|
||||||
let mut command = Command::new(program_path);
|
let mut command = Command::new(program_path);
|
||||||
@@ -147,6 +153,7 @@ impl AcpAgentHarness {
|
|||||||
self.session_namespace.clone(),
|
self.session_namespace.clone(),
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.mode.clone(),
|
self.mode.clone(),
|
||||||
|
approvals,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -167,6 +174,7 @@ impl AcpAgentHarness {
|
|||||||
session_namespace: String,
|
session_namespace: String,
|
||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
mode: Option<String>,
|
mode: Option<String>,
|
||||||
|
approvals: Option<std::sync::Arc<dyn ExecutorApprovalService>>,
|
||||||
) -> Result<(), ExecutorError> {
|
) -> Result<(), ExecutorError> {
|
||||||
// Take child's stdio for ACP wiring
|
// Take child's stdio for ACP wiring
|
||||||
let orig_stdout = child.inner().stdout.take().ok_or_else(|| {
|
let orig_stdout = child.inner().stdout.take().ok_or_else(|| {
|
||||||
@@ -281,8 +289,9 @@ impl AcpAgentHarness {
|
|||||||
};
|
};
|
||||||
let session_manager = std::sync::Arc::new(session_manager);
|
let session_manager = std::sync::Arc::new(session_manager);
|
||||||
|
|
||||||
// Create ACP client
|
// Create ACP client with approvals support
|
||||||
let client = AcpClient::new(event_tx.clone());
|
let client = AcpClient::new(event_tx.clone(), approvals.clone());
|
||||||
|
let client_feedback_handle = client.clone();
|
||||||
|
|
||||||
client.record_user_prompt_event(&prompt);
|
client.record_user_prompt_event(&prompt);
|
||||||
|
|
||||||
@@ -291,6 +300,7 @@ impl AcpAgentHarness {
|
|||||||
proto::ClientSideConnection::new(client, outgoing, incoming, |fut| {
|
proto::ClientSideConnection::new(client, outgoing, incoming, |fut| {
|
||||||
tokio::task::spawn_local(fut);
|
tokio::task::spawn_local(fut);
|
||||||
});
|
});
|
||||||
|
let conn = Rc::new(conn);
|
||||||
|
|
||||||
// Drive I/O
|
// Drive I/O
|
||||||
let io_handle = tokio::task::spawn_local(async move {
|
let io_handle = tokio::task::spawn_local(async move {
|
||||||
@@ -382,13 +392,30 @@ impl AcpAgentHarness {
|
|||||||
let app_tx_clone = log_tx.clone();
|
let app_tx_clone = log_tx.clone();
|
||||||
let sess_id_for_writer = display_session_id.clone();
|
let sess_id_for_writer = display_session_id.clone();
|
||||||
let sm_for_writer = session_manager.clone();
|
let sm_for_writer = session_manager.clone();
|
||||||
tokio::spawn(async move {
|
let conn_for_cancel = conn.clone();
|
||||||
|
let acp_session_id_for_cancel = acp_session_id.clone();
|
||||||
|
tokio::task::spawn_local(async move {
|
||||||
while let Some(event) = event_rx.recv().await {
|
while let Some(event) = event_rx.recv().await {
|
||||||
|
if let AcpEvent::ApprovalResponse(resp) = &event
|
||||||
|
&& let ApprovalStatus::Denied {
|
||||||
|
reason: Some(reason),
|
||||||
|
} = &resp.status
|
||||||
|
&& !reason.trim().is_empty()
|
||||||
|
{
|
||||||
|
let _ = conn_for_cancel
|
||||||
|
.cancel(proto::CancelNotification::new(
|
||||||
|
proto::SessionId::new(
|
||||||
|
acp_session_id_for_cancel.clone(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = event.to_string();
|
||||||
// Forward to stdout
|
// Forward to stdout
|
||||||
let _ = app_tx_clone.send(event.to_string());
|
let _ = app_tx_clone.send(line.clone());
|
||||||
// Persist to session file
|
// Persist to session file
|
||||||
let _ = sm_for_writer
|
let _ = sm_for_writer.append_raw_line(&sess_id_for_writer, &line);
|
||||||
.append_raw_line(&sess_id_for_writer, &event.to_string());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -400,35 +427,61 @@ impl AcpAgentHarness {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Build prompt request
|
// Build prompt request
|
||||||
let req = proto::PromptRequest::new(
|
let initial_req = proto::PromptRequest::new(
|
||||||
proto::SessionId::new(acp_session_id.clone()),
|
proto::SessionId::new(acp_session_id.clone()),
|
||||||
vec![proto::ContentBlock::Text(proto::TextContent::new(
|
vec![proto::ContentBlock::Text(proto::TextContent::new(
|
||||||
prompt_to_send,
|
prompt_to_send,
|
||||||
))],
|
))],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send the prompt and await completion to obtain stop_reason
|
let mut current_req = Some(initial_req);
|
||||||
match conn.prompt(req).await {
|
|
||||||
Ok(resp) => {
|
while let Some(req) = current_req.take() {
|
||||||
// Emit done with stop_reason
|
tracing::trace!(?req, "sending ACP prompt request");
|
||||||
let stop_reason =
|
// Send the prompt and await completion to obtain stop_reason
|
||||||
serde_json::to_string(&resp.stop_reason).unwrap_or_default();
|
match conn.prompt(req).await {
|
||||||
let _ = log_tx.send(AcpEvent::Done(stop_reason).to_string());
|
Ok(resp) => {
|
||||||
}
|
// Emit done with stop_reason
|
||||||
Err(e) => {
|
let stop_reason = serde_json::to_string(&resp.stop_reason)
|
||||||
tracing::debug!("error {} {e} {:?}", e.code, e.data);
|
.unwrap_or_default();
|
||||||
if e.code == agent_client_protocol::ErrorCode::INTERNAL_ERROR.code
|
let _ = log_tx.send(AcpEvent::Done(stop_reason).to_string());
|
||||||
&& e.data
|
}
|
||||||
.as_ref()
|
Err(e) => {
|
||||||
.is_some_and(|d| d == "server shut down unexpectedly")
|
tracing::debug!("error {} {e} {:?}", e.code, e.data);
|
||||||
{
|
if e.code
|
||||||
tracing::debug!("ACP server killed");
|
== agent_client_protocol::ErrorCode::INTERNAL_ERROR.code
|
||||||
} else {
|
&& e.data
|
||||||
let _ =
|
.as_ref()
|
||||||
log_tx.send(AcpEvent::Error(format!("{e}")).to_string());
|
.is_some_and(|d| d == "server shut down unexpectedly")
|
||||||
|
{
|
||||||
|
tracing::debug!("ACP server killed");
|
||||||
|
} else {
|
||||||
|
let _ = log_tx
|
||||||
|
.send(AcpEvent::Error(format!("{e}")).to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush any pending user feedback after finish
|
||||||
|
let feedback = client_feedback_handle
|
||||||
|
.drain_feedback()
|
||||||
|
.await
|
||||||
|
.join("\n")
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if !feedback.is_empty() {
|
||||||
|
tracing::trace!(?feedback, "sending ACP follow-up feedback");
|
||||||
|
let session_id = proto::SessionId::new(acp_session_id.clone());
|
||||||
|
let feedback_req = proto::PromptRequest::new(
|
||||||
|
session_id.clone(),
|
||||||
|
vec![proto::ContentBlock::Text(proto::TextContent::new(
|
||||||
|
feedback,
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
current_req = Some(feedback_req);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify container of completion
|
// Notify container of completion
|
||||||
if let Some(tx) = exit_signal_tx.take() {
|
if let Some(tx) = exit_signal_tx.take() {
|
||||||
let _ = tx.send(ExecutorExitResult::Success);
|
let _ = tx.send(ExecutorExitResult::Success);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub use harness::AcpAgentHarness;
|
|||||||
pub use normalize_logs::*;
|
pub use normalize_logs::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub use session::SessionManager;
|
pub use session::SessionManager;
|
||||||
|
use workspace_utils::approvals::ApprovalStatus;
|
||||||
|
|
||||||
/// Parsed event types for internal processing
|
/// Parsed event types for internal processing
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -24,6 +25,7 @@ pub enum AcpEvent {
|
|||||||
AvailableCommands(Vec<agent_client_protocol::AvailableCommand>),
|
AvailableCommands(Vec<agent_client_protocol::AvailableCommand>),
|
||||||
CurrentMode(agent_client_protocol::SessionModeId),
|
CurrentMode(agent_client_protocol::SessionModeId),
|
||||||
RequestPermission(agent_client_protocol::RequestPermissionRequest),
|
RequestPermission(agent_client_protocol::RequestPermissionRequest),
|
||||||
|
ApprovalResponse(ApprovalResponse),
|
||||||
Error(String),
|
Error(String),
|
||||||
Done(String),
|
Done(String),
|
||||||
Other(agent_client_protocol::SessionNotification),
|
Other(agent_client_protocol::SessionNotification),
|
||||||
@@ -42,3 +44,9 @@ impl FromStr for AcpEvent {
|
|||||||
serde_json::from_str(s)
|
serde_json::from_str(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ApprovalResponse {
|
||||||
|
pub tool_call_id: String,
|
||||||
|
pub status: ApprovalStatus,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,15 +8,18 @@ use agent_client_protocol::{self as acp, SessionNotification};
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace_utils::msg_store::MsgStore;
|
use workspace_utils::{approvals::ApprovalStatus, msg_store::MsgStore};
|
||||||
|
|
||||||
pub use super::AcpAgentHarness;
|
pub use super::AcpAgentHarness;
|
||||||
use super::AcpEvent;
|
use super::AcpEvent;
|
||||||
use crate::logs::{
|
use crate::{
|
||||||
ActionType, FileChange, NormalizedEntry, NormalizedEntryError, NormalizedEntryType, TodoItem,
|
approvals::ToolCallMetadata,
|
||||||
ToolResult, ToolResultValueType, ToolStatus as LogToolStatus,
|
logs::{
|
||||||
stderr_processor::normalize_stderr_logs,
|
ActionType, FileChange, NormalizedEntry, NormalizedEntryError, NormalizedEntryType,
|
||||||
utils::{ConversationPatch, EntryIndexProvider},
|
TodoItem, ToolResult, ToolResultValueType, ToolStatus as LogToolStatus,
|
||||||
|
stderr_processor::normalize_stderr_logs,
|
||||||
|
utils::{ConversationPatch, EntryIndexProvider},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn normalize_logs(msg_store: Arc<MsgStore>, worktree_path: &Path) {
|
pub fn normalize_logs(msg_store: Arc<MsgStore>, worktree_path: &Path) {
|
||||||
@@ -220,6 +223,35 @@ pub fn normalize_logs(msg_store: Arc<MsgStore>, worktree_path: &Path) {
|
|||||||
tracing::debug!("Failed to convert tool call update to ToolCall");
|
tracing::debug!("Failed to convert tool call update to ToolCall");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AcpEvent::ApprovalResponse(resp) => {
|
||||||
|
tracing::trace!("Received approval response: {:?}", resp);
|
||||||
|
if let ApprovalStatus::Denied { reason } = resp.status {
|
||||||
|
let tool_name = tool_states
|
||||||
|
.get(&resp.tool_call_id)
|
||||||
|
.map(|t| {
|
||||||
|
extract_tool_name_from_id(t.id.0.as_ref())
|
||||||
|
.unwrap_or_else(|| t.title.clone())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let idx = entry_index.next();
|
||||||
|
let entry = NormalizedEntry {
|
||||||
|
timestamp: None,
|
||||||
|
entry_type: NormalizedEntryType::UserFeedback {
|
||||||
|
denied_tool: tool_name,
|
||||||
|
},
|
||||||
|
content: reason
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
"User denied this tool use request".to_string()
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
metadata: None,
|
||||||
|
};
|
||||||
|
msg_store
|
||||||
|
.push_patch(ConversationPatch::add_normalized_entry(idx, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
AcpEvent::User(_) | AcpEvent::Other(_) => (),
|
AcpEvent::User(_) | AcpEvent::Other(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +283,10 @@ pub fn normalize_logs(msg_store: Arc<MsgStore>, worktree_path: &Path) {
|
|||||||
status: convert_tool_status(&tool_data.status),
|
status: convert_tool_status(&tool_data.status),
|
||||||
},
|
},
|
||||||
content: get_tool_content(tool_data),
|
content: get_tool_content(tool_data),
|
||||||
metadata: None,
|
metadata: serde_json::to_value(ToolCallMetadata {
|
||||||
|
tool_call_id: tool_data.id.0.to_string(),
|
||||||
|
})
|
||||||
|
.ok(),
|
||||||
};
|
};
|
||||||
let patch = if is_new {
|
let patch = if is_new {
|
||||||
ConversationPatch::add_normalized_entry(tool_data.index, entry)
|
ConversationPatch::add_normalized_entry(tool_data.index, entry)
|
||||||
@@ -461,6 +496,31 @@ pub fn normalize_logs(msg_store: Arc<MsgStore>, worktree_path: &Path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if changes.is_empty()
|
||||||
|
&& let Some(raw) = &tc.raw_input
|
||||||
|
&& let Ok(edit_input) = serde_json::from_value::<EditInput>(raw.clone())
|
||||||
|
{
|
||||||
|
if let Some(diff) = edit_input.diff {
|
||||||
|
changes.push(FileChange::Edit {
|
||||||
|
unified_diff: workspace_utils::diff::normalize_unified_diff(
|
||||||
|
&edit_input.file_path,
|
||||||
|
&diff,
|
||||||
|
),
|
||||||
|
has_line_numbers: true,
|
||||||
|
});
|
||||||
|
} else if let Some(old) = edit_input.old_string
|
||||||
|
&& let Some(new) = edit_input.new_string
|
||||||
|
{
|
||||||
|
changes.push(FileChange::Edit {
|
||||||
|
unified_diff: workspace_utils::diff::create_unified_diff(
|
||||||
|
&edit_input.file_path,
|
||||||
|
&old,
|
||||||
|
&new,
|
||||||
|
),
|
||||||
|
has_line_numbers: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
changes
|
changes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,3 +747,15 @@ struct StreamingText {
|
|||||||
index: usize,
|
index: usize,
|
||||||
content: String,
|
content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct EditInput {
|
||||||
|
file_path: String,
|
||||||
|
#[serde(default)]
|
||||||
|
diff: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
old_string: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
new_string: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ impl SessionManager {
|
|||||||
| AcpEvent::ToolUpdate(..)
|
| AcpEvent::ToolUpdate(..)
|
||||||
| AcpEvent::Plan(..)
|
| AcpEvent::Plan(..)
|
||||||
| AcpEvent::AvailableCommands(..)
|
| AcpEvent::AvailableCommands(..)
|
||||||
|
| AcpEvent::ApprovalResponse(..)
|
||||||
| AcpEvent::CurrentMode(..) => {}
|
| AcpEvent::CurrentMode(..) => {}
|
||||||
|
|
||||||
AcpEvent::RequestPermission(req) => event = AcpEvent::ToolUpdate(req.tool_call),
|
AcpEvent::RequestPermission(req) => event = AcpEvent::ToolUpdate(req.tool_call),
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ use regex::Regex;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use workspace_utils::{
|
use workspace_utils::{
|
||||||
approvals::ApprovalStatus,
|
approvals::ApprovalStatus, diff::normalize_unified_diff, msg_store::MsgStore,
|
||||||
diff::{concatenate_diff_hunks, extract_unified_diff_hunks},
|
|
||||||
msg_store::MsgStore,
|
|
||||||
path::make_path_relative,
|
path::make_path_relative,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -337,8 +335,7 @@ fn normalize_file_changes(
|
|||||||
make_path_relative(dest.to_string_lossy().as_ref(), worktree_path);
|
make_path_relative(dest.to_string_lossy().as_ref(), worktree_path);
|
||||||
edits.push(FileChange::Rename { new_path: dest_rel });
|
edits.push(FileChange::Rename { new_path: dest_rel });
|
||||||
}
|
}
|
||||||
let hunks = extract_unified_diff_hunks(unified_diff);
|
let diff = normalize_unified_diff(&relative, unified_diff);
|
||||||
let diff = concatenate_diff_hunks(&relative, &hunks);
|
|
||||||
edits.push(FileChange::Edit {
|
edits.push(FileChange::Edit {
|
||||||
unified_diff: diff,
|
unified_diff: diff,
|
||||||
has_line_numbers: true,
|
has_line_numbers: true,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tokio::{io::AsyncWriteExt, process::Command};
|
use tokio::{io::AsyncWriteExt, process::Command};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use workspace_utils::{
|
use workspace_utils::{
|
||||||
diff::{concatenate_diff_hunks, create_unified_diff, extract_unified_diff_hunks},
|
diff::{create_unified_diff, normalize_unified_diff},
|
||||||
msg_store::MsgStore,
|
msg_store::MsgStore,
|
||||||
path::make_path_relative,
|
path::make_path_relative,
|
||||||
shell::resolve_executable_path_blocking,
|
shell::resolve_executable_path_blocking,
|
||||||
@@ -734,9 +734,8 @@ impl CursorToolCall {
|
|||||||
let mut changes = vec![];
|
let mut changes = vec![];
|
||||||
|
|
||||||
if let Some(apply_patch) = &args.apply_patch {
|
if let Some(apply_patch) = &args.apply_patch {
|
||||||
let hunks = extract_unified_diff_hunks(&apply_patch.patch_content);
|
|
||||||
changes.push(FileChange::Edit {
|
changes.push(FileChange::Edit {
|
||||||
unified_diff: concatenate_diff_hunks(&path, &hunks),
|
unified_diff: normalize_unified_diff(&path, &apply_patch.patch_content),
|
||||||
has_line_numbers: false,
|
has_line_numbers: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -774,9 +773,8 @@ impl CursorToolCall {
|
|||||||
..
|
..
|
||||||
})) = &result
|
})) = &result
|
||||||
{
|
{
|
||||||
let hunks = extract_unified_diff_hunks(diff_string);
|
|
||||||
changes.push(FileChange::Edit {
|
changes.push(FileChange::Edit {
|
||||||
unified_diff: concatenate_diff_hunks(&path, &hunks),
|
unified_diff: normalize_unified_diff(&path, diff_string),
|
||||||
has_line_numbers: false,
|
has_line_numbers: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ use futures::{StreamExt, future::ready};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use workspace_utils::{
|
use workspace_utils::{
|
||||||
diff::{concatenate_diff_hunks, extract_unified_diff_hunks},
|
diff::normalize_unified_diff, msg_store::MsgStore, path::make_path_relative,
|
||||||
msg_store::MsgStore,
|
|
||||||
path::make_path_relative,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::logs::{
|
use crate::logs::{
|
||||||
@@ -767,9 +765,8 @@ fn parse_apply_patch_result(value: &Value, worktree_path: &str) -> Option<Action
|
|||||||
let relative_path = make_path_relative(&file_path, worktree_path);
|
let relative_path = make_path_relative(&file_path, worktree_path);
|
||||||
|
|
||||||
let changes = if let Some(diff_text) = diff {
|
let changes = if let Some(diff_text) = diff {
|
||||||
let hunks = extract_unified_diff_hunks(&diff_text);
|
|
||||||
vec![FileChange::Edit {
|
vec![FileChange::Edit {
|
||||||
unified_diff: concatenate_diff_hunks(&relative_path, &hunks),
|
unified_diff: normalize_unified_diff(&relative_path, &diff_text),
|
||||||
has_line_numbers: true,
|
has_line_numbers: true,
|
||||||
}]
|
}]
|
||||||
} else if let Some(content_text) = content {
|
} else if let Some(content_text) = content {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use derivative::Derivative;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
@@ -8,6 +9,7 @@ use workspace_utils::msg_store::MsgStore;
|
|||||||
|
|
||||||
pub use super::acp::AcpAgentHarness;
|
pub use super::acp::AcpAgentHarness;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
approvals::ExecutorApprovalService,
|
||||||
command::{CmdOverrides, CommandBuilder, apply_overrides},
|
command::{CmdOverrides, CommandBuilder, apply_overrides},
|
||||||
env::ExecutionEnv,
|
env::ExecutionEnv,
|
||||||
executors::{
|
executors::{
|
||||||
@@ -15,7 +17,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
|
#[derive(Derivative, Clone, Serialize, Deserialize, TS, JsonSchema)]
|
||||||
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct Gemini {
|
pub struct Gemini {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub append_prompt: AppendPrompt,
|
pub append_prompt: AppendPrompt,
|
||||||
@@ -25,6 +28,10 @@ pub struct Gemini {
|
|||||||
pub yolo: Option<bool>,
|
pub yolo: Option<bool>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub cmd: CmdOverrides,
|
pub cmd: CmdOverrides,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[ts(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub approvals: Option<Arc<dyn ExecutorApprovalService>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gemini {
|
impl Gemini {
|
||||||
@@ -49,6 +56,10 @@ impl Gemini {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl StandardCodingAgentExecutor for Gemini {
|
impl StandardCodingAgentExecutor for Gemini {
|
||||||
|
fn use_approvals(&mut self, approvals: Arc<dyn ExecutorApprovalService>) {
|
||||||
|
self.approvals = Some(approvals);
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn(
|
async fn spawn(
|
||||||
&self,
|
&self,
|
||||||
current_dir: &Path,
|
current_dir: &Path,
|
||||||
@@ -58,8 +69,20 @@ impl StandardCodingAgentExecutor for Gemini {
|
|||||||
let harness = AcpAgentHarness::new();
|
let harness = AcpAgentHarness::new();
|
||||||
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
||||||
let gemini_command = self.build_command_builder().build_initial()?;
|
let gemini_command = self.build_command_builder().build_initial()?;
|
||||||
|
let approvals = if self.yolo.unwrap_or(false) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.approvals.clone()
|
||||||
|
};
|
||||||
harness
|
harness
|
||||||
.spawn_with_command(current_dir, combined_prompt, gemini_command, env, &self.cmd)
|
.spawn_with_command(
|
||||||
|
current_dir,
|
||||||
|
combined_prompt,
|
||||||
|
gemini_command,
|
||||||
|
env,
|
||||||
|
&self.cmd,
|
||||||
|
approvals,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +96,11 @@ impl StandardCodingAgentExecutor for Gemini {
|
|||||||
let harness = AcpAgentHarness::new();
|
let harness = AcpAgentHarness::new();
|
||||||
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
||||||
let gemini_command = self.build_command_builder().build_follow_up(&[])?;
|
let gemini_command = self.build_command_builder().build_follow_up(&[])?;
|
||||||
|
let approvals = if self.yolo.unwrap_or(false) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.approvals.clone()
|
||||||
|
};
|
||||||
harness
|
harness
|
||||||
.spawn_follow_up_with_command(
|
.spawn_follow_up_with_command(
|
||||||
current_dir,
|
current_dir,
|
||||||
@@ -81,6 +109,7 @@ impl StandardCodingAgentExecutor for Gemini {
|
|||||||
gemini_command,
|
gemini_command,
|
||||||
env,
|
env,
|
||||||
&self.cmd,
|
&self.cmd,
|
||||||
|
approvals,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use derivative::Derivative;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use workspace_utils::msg_store::MsgStore;
|
use workspace_utils::msg_store::MsgStore;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
approvals::ExecutorApprovalService,
|
||||||
command::{CmdOverrides, CommandBuilder, apply_overrides},
|
command::{CmdOverrides, CommandBuilder, apply_overrides},
|
||||||
env::ExecutionEnv,
|
env::ExecutionEnv,
|
||||||
executors::{
|
executors::{
|
||||||
@@ -15,7 +17,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
|
#[derive(Derivative, Clone, Serialize, Deserialize, TS, JsonSchema)]
|
||||||
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct Opencode {
|
pub struct Opencode {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub append_prompt: AppendPrompt,
|
pub append_prompt: AppendPrompt,
|
||||||
@@ -23,13 +26,20 @@ pub struct Opencode {
|
|||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none", alias = "agent")]
|
#[serde(default, skip_serializing_if = "Option::is_none", alias = "agent")]
|
||||||
pub mode: Option<String>,
|
pub mode: Option<String>,
|
||||||
|
/// Auto-approve agent actions
|
||||||
|
#[serde(default = "default_to_true")]
|
||||||
|
pub auto_approve: bool,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub cmd: CmdOverrides,
|
pub cmd: CmdOverrides,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[ts(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub approvals: Option<Arc<dyn ExecutorApprovalService>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Opencode {
|
impl Opencode {
|
||||||
fn build_command_builder(&self) -> CommandBuilder {
|
fn build_command_builder(&self) -> CommandBuilder {
|
||||||
let builder = CommandBuilder::new("npx -y opencode-ai@1.0.134 acp");
|
let builder = CommandBuilder::new("npx -y opencode-ai@1.0.134").extend_params(["acp"]);
|
||||||
apply_overrides(builder, &self.cmd)
|
apply_overrides(builder, &self.cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +50,10 @@ impl Opencode {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl StandardCodingAgentExecutor for Opencode {
|
impl StandardCodingAgentExecutor for Opencode {
|
||||||
|
fn use_approvals(&mut self, approvals: Arc<dyn ExecutorApprovalService>) {
|
||||||
|
self.approvals = Some(approvals);
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn(
|
async fn spawn(
|
||||||
&self,
|
&self,
|
||||||
current_dir: &Path,
|
current_dir: &Path,
|
||||||
@@ -56,13 +70,20 @@ impl StandardCodingAgentExecutor for Opencode {
|
|||||||
harness = harness.with_mode(agent);
|
harness = harness.with_mode(agent);
|
||||||
}
|
}
|
||||||
let opencode_command = self.build_command_builder().build_initial()?;
|
let opencode_command = self.build_command_builder().build_initial()?;
|
||||||
|
let approvals = if self.auto_approve {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.approvals.clone()
|
||||||
|
};
|
||||||
|
let env = setup_approvals_env(self.auto_approve, env);
|
||||||
harness
|
harness
|
||||||
.spawn_with_command(
|
.spawn_with_command(
|
||||||
current_dir,
|
current_dir,
|
||||||
combined_prompt,
|
combined_prompt,
|
||||||
opencode_command,
|
opencode_command,
|
||||||
env,
|
&env,
|
||||||
&self.cmd,
|
&self.cmd,
|
||||||
|
approvals,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -83,14 +104,21 @@ impl StandardCodingAgentExecutor for Opencode {
|
|||||||
harness = harness.with_mode(agent);
|
harness = harness.with_mode(agent);
|
||||||
}
|
}
|
||||||
let opencode_command = self.build_command_builder().build_follow_up(&[])?;
|
let opencode_command = self.build_command_builder().build_follow_up(&[])?;
|
||||||
|
let approvals = if self.auto_approve {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.approvals.clone()
|
||||||
|
};
|
||||||
|
let env = setup_approvals_env(self.auto_approve, env);
|
||||||
harness
|
harness
|
||||||
.spawn_follow_up_with_command(
|
.spawn_follow_up_with_command(
|
||||||
current_dir,
|
current_dir,
|
||||||
combined_prompt,
|
combined_prompt,
|
||||||
session_id,
|
session_id,
|
||||||
opencode_command,
|
opencode_command,
|
||||||
env,
|
&env,
|
||||||
&self.cmd,
|
&self.cmd,
|
||||||
|
approvals,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -127,3 +155,15 @@ impl StandardCodingAgentExecutor for Opencode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_to_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_approvals_env(auto_approve: bool, env: &ExecutionEnv) -> ExecutionEnv {
|
||||||
|
let mut env = env.clone();
|
||||||
|
if !auto_approve && !env.contains_key("OPENCODE_PERMISSION") {
|
||||||
|
env.insert("OPENCODE_PERMISSION", r#"{"edit": "ask", "bash": "ask", "webfetch": "ask", "doom_loop": "ask", "external_directory": "ask"}"#);
|
||||||
|
}
|
||||||
|
env
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use derivative::Derivative;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use workspace_utils::msg_store::MsgStore;
|
use workspace_utils::msg_store::MsgStore;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
approvals::ExecutorApprovalService,
|
||||||
command::{CmdOverrides, CommandBuilder, apply_overrides},
|
command::{CmdOverrides, CommandBuilder, apply_overrides},
|
||||||
env::ExecutionEnv,
|
env::ExecutionEnv,
|
||||||
executors::{
|
executors::{
|
||||||
@@ -15,7 +17,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
|
#[derive(Derivative, Clone, Serialize, Deserialize, TS, JsonSchema)]
|
||||||
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct QwenCode {
|
pub struct QwenCode {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub append_prompt: AppendPrompt,
|
pub append_prompt: AppendPrompt,
|
||||||
@@ -23,6 +26,10 @@ pub struct QwenCode {
|
|||||||
pub yolo: Option<bool>,
|
pub yolo: Option<bool>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub cmd: CmdOverrides,
|
pub cmd: CmdOverrides,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[ts(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub approvals: Option<Arc<dyn ExecutorApprovalService>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QwenCode {
|
impl QwenCode {
|
||||||
@@ -39,6 +46,10 @@ impl QwenCode {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl StandardCodingAgentExecutor for QwenCode {
|
impl StandardCodingAgentExecutor for QwenCode {
|
||||||
|
fn use_approvals(&mut self, approvals: Arc<dyn ExecutorApprovalService>) {
|
||||||
|
self.approvals = Some(approvals);
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn(
|
async fn spawn(
|
||||||
&self,
|
&self,
|
||||||
current_dir: &Path,
|
current_dir: &Path,
|
||||||
@@ -48,8 +59,20 @@ impl StandardCodingAgentExecutor for QwenCode {
|
|||||||
let qwen_command = self.build_command_builder().build_initial()?;
|
let qwen_command = self.build_command_builder().build_initial()?;
|
||||||
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
||||||
let harness = AcpAgentHarness::with_session_namespace("qwen_sessions");
|
let harness = AcpAgentHarness::with_session_namespace("qwen_sessions");
|
||||||
|
let approvals = if self.yolo.unwrap_or(false) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.approvals.clone()
|
||||||
|
};
|
||||||
harness
|
harness
|
||||||
.spawn_with_command(current_dir, combined_prompt, qwen_command, env, &self.cmd)
|
.spawn_with_command(
|
||||||
|
current_dir,
|
||||||
|
combined_prompt,
|
||||||
|
qwen_command,
|
||||||
|
env,
|
||||||
|
&self.cmd,
|
||||||
|
approvals,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +86,11 @@ impl StandardCodingAgentExecutor for QwenCode {
|
|||||||
let qwen_command = self.build_command_builder().build_follow_up(&[])?;
|
let qwen_command = self.build_command_builder().build_follow_up(&[])?;
|
||||||
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
let combined_prompt = self.append_prompt.combine_prompt(prompt);
|
||||||
let harness = AcpAgentHarness::with_session_namespace("qwen_sessions");
|
let harness = AcpAgentHarness::with_session_namespace("qwen_sessions");
|
||||||
|
let approvals = if self.yolo.unwrap_or(false) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.approvals.clone()
|
||||||
|
};
|
||||||
harness
|
harness
|
||||||
.spawn_follow_up_with_command(
|
.spawn_follow_up_with_command(
|
||||||
current_dir,
|
current_dir,
|
||||||
@@ -71,6 +99,7 @@ impl StandardCodingAgentExecutor for QwenCode {
|
|||||||
qwen_command,
|
qwen_command,
|
||||||
env,
|
env,
|
||||||
&self.cmd,
|
&self.cmd,
|
||||||
|
approvals,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -972,14 +972,18 @@ impl ContainerService for LocalContainerService {
|
|||||||
|
|
||||||
let approvals_service: Arc<dyn ExecutorApprovalService> =
|
let approvals_service: Arc<dyn ExecutorApprovalService> =
|
||||||
match executor_action.base_executor() {
|
match executor_action.base_executor() {
|
||||||
Some(BaseCodingAgent::Codex) | Some(BaseCodingAgent::ClaudeCode) => {
|
Some(
|
||||||
ExecutorApprovalBridge::new(
|
BaseCodingAgent::Codex
|
||||||
self.approvals.clone(),
|
| BaseCodingAgent::ClaudeCode
|
||||||
self.db.clone(),
|
| BaseCodingAgent::Gemini
|
||||||
self.notification_service.clone(),
|
| BaseCodingAgent::QwenCode
|
||||||
execution_process.id,
|
| BaseCodingAgent::Opencode,
|
||||||
)
|
) => ExecutorApprovalBridge::new(
|
||||||
}
|
self.approvals.clone(),
|
||||||
|
self.db.clone(),
|
||||||
|
self.notification_service.clone(),
|
||||||
|
execution_process.id,
|
||||||
|
),
|
||||||
_ => Arc::new(NoopExecutorApprovalService {}),
|
_ => Arc::new(NoopExecutorApprovalService {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -238,3 +238,9 @@ pub fn concatenate_diff_hunks(file_path: &str, hunks: &[String]) -> String {
|
|||||||
|
|
||||||
unified_diff
|
unified_diff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes a unified diff the format supported by the diff viewer,
|
||||||
|
pub fn normalize_unified_diff(file_path: &str, unified_diff: &str) -> String {
|
||||||
|
let hunks = extract_unified_diff_hunks(unified_diff);
|
||||||
|
concatenate_diff_hunks(file_path, &hunks)
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"auto_approve": {
|
||||||
|
"description": "Auto-approve agent actions",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
"base_command_override": {
|
"base_command_override": {
|
||||||
"title": "Base Command Override",
|
"title": "Base Command Override",
|
||||||
"description": "Override the base command with a custom command",
|
"description": "Override the base command with a custom command",
|
||||||
|
|||||||
@@ -378,7 +378,11 @@ export type CursorAgent = { append_prompt: AppendPrompt, force?: boolean | null,
|
|||||||
|
|
||||||
export type Copilot = { append_prompt: AppendPrompt, model?: string | null, allow_all_tools?: boolean | null, allow_tool?: string | null, deny_tool?: string | null, add_dir?: Array<string> | null, disable_mcp_server?: Array<string> | null, base_command_override?: string | null, additional_params?: Array<string> | null, env?: { [key in string]?: string } | null, };
|
export type Copilot = { append_prompt: AppendPrompt, model?: string | null, allow_all_tools?: boolean | null, allow_tool?: string | null, deny_tool?: string | null, add_dir?: Array<string> | null, disable_mcp_server?: Array<string> | null, base_command_override?: string | null, additional_params?: Array<string> | null, env?: { [key in string]?: string } | null, };
|
||||||
|
|
||||||
export type Opencode = { append_prompt: AppendPrompt, model?: string | null, mode?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, env?: { [key in string]?: string } | null, };
|
export type Opencode = { append_prompt: AppendPrompt, model?: string | null, mode?: string | null,
|
||||||
|
/**
|
||||||
|
* Auto-approve agent actions
|
||||||
|
*/
|
||||||
|
auto_approve: boolean, base_command_override?: string | null, additional_params?: Array<string> | null, env?: { [key in string]?: string } | null, };
|
||||||
|
|
||||||
export type QwenCode = { append_prompt: AppendPrompt, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, env?: { [key in string]?: string } | null, };
|
export type QwenCode = { append_prompt: AppendPrompt, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, env?: { [key in string]?: string } | null, };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user