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
16 changes: 16 additions & 0 deletions crates/zeroclaw-api/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ pub enum TurnEvent {
name: String,
output: String,
},
/// The agent is waiting for the operator to approve, deny, or always-allow
/// a tool call. The transport (e.g. gateway WebSocket) is expected to
/// surface this to the operator and route the response back through the
/// same correlation `request_id`. The runtime tool loop pauses until that
/// answer arrives or the channel times out.
ApprovalRequest {
/// Correlation ID. The matching response frame must echo it.
request_id: String,
tool_name: String,
/// Human-readable, secret-redacted summary of the tool arguments.
/// Synthesised by `crate::approval::summarize_args`; never the raw
/// `args` value.
arguments_summary: String,
/// How long the channel will wait before auto-denying.
timeout_secs: u64,
},
}
23 changes: 16 additions & 7 deletions crates/zeroclaw-channels/src/orchestrator/acp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,8 +683,9 @@ impl AcpServer {
if let TurnEvent::Chunk { ref delta } = event {
accumulated_text.push_str(delta);
}
let notification = notification_for_turn_event(&session_id, &event);
self.write_notification(&notification).await;
if let Some(notification) = notification_for_turn_event(&session_id, &event) {
self.write_notification(&notification).await;
}
}

// Remove the cancel token regardless of outcome — the turn is over.
Expand Down Expand Up @@ -1005,8 +1006,8 @@ fn map_tool_kind(name: &str) -> &'static str {
}
}

fn notification_for_turn_event(session_id: &str, event: &TurnEvent) -> JsonRpcNotification {
match event {
fn notification_for_turn_event(session_id: &str, event: &TurnEvent) -> Option<JsonRpcNotification> {
Some(match event {
TurnEvent::Chunk { delta } => JsonRpcNotification {
jsonrpc: "2.0",
method: "session/update",
Expand Down Expand Up @@ -1075,7 +1076,13 @@ fn notification_for_turn_event(session_id: &str, event: &TurnEvent) -> JsonRpcNo
}
}),
},
}
// ACP has its own approval mechanism via `session/request_permission`
// routed through the channel's `request_choice` impl. The agent only
// emits ApprovalRequest events when a back-channel like the gateway
// WS is registered to handle them; on ACP-only sessions they should
// not arrive here.
TurnEvent::ApprovalRequest { .. } => return None,
})
}

// ── Error helper ─────────────────────────────────────────────────
Expand Down Expand Up @@ -1460,7 +1467,8 @@ mod tests {
args: serde_json::json!({"command": "ls -la"}),
},
);
let call_value = serde_json::to_value(call).unwrap();
let call_value =
serde_json::to_value(call.expect("ToolCall maps to a notification")).unwrap();
assert_eq!(call_value["method"], "session/update");
assert_eq!(call_value["params"]["update"]["sessionUpdate"], "tool_call");
assert_eq!(call_value["params"]["update"]["toolCallId"], "tc-12345");
Expand All @@ -1480,7 +1488,8 @@ mod tests {
output: "file1.txt\nfile2.txt".to_string(),
},
);
let result_value = serde_json::to_value(result).unwrap();
let result_value =
serde_json::to_value(result.expect("ToolResult maps to a notification")).unwrap();
assert_eq!(
result_value["params"]["update"]["sessionUpdate"],
"tool_call_update"
Expand Down
1 change: 1 addition & 0 deletions crates/zeroclaw-gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod tls;
#[cfg(feature = "gateway-voice-duplex")]
pub mod voice_duplex;
pub mod ws;
pub mod ws_approval;

use anyhow::{Context, Result};
use axum::{
Expand Down
Loading
Loading