- 1
//! Human-In-The-Loop (HIL) state machine with in-place command editing. - 2
//! - 3
//! A real `HilApprovalState` is populated from an `AgentEvent::ApprovalRequested` - 4
//! that arrives over the SSE event stream, not from a hardcoded demo fixture. - 5
//! When the operator approves or denies, the outcome is dispatched to the - 6
//! server via `POST /sessions/{id}/approvals/{req_id}` — there is no local - 7
//! state change that never reaches the server. - 8
- 9
use serde_json::Value; - 10
- 11
#[derive(Debug, Clone, PartialEq, Eq)] - 12
pub enum HilOutcome { - 13
ApproveOnce { modified_command: Option<String> }, - 14
AlwaysForSession, - 15
Deny, - 16
Cancel, - 17
} - 18
- 19
/// Represents whether a "remember this decision" checkbox is active. - 20
#[derive(Debug, Clone, Copy, PartialEq, Eq)] - 21
pub enum RememberMode { - 22
No, - 23
Yes, - 24
} - 25
- 26
/// A real approval request that arrived over the SSE event stream. - 27
#[derive(Debug, Clone)] - 28
pub struct HilApprovalState { - 29
pub request_id: String, - 30
pub session_id: String, - 31
pub tool_name: String, - 32
pub args_json: String, - 33
pub risk_reason: String, - 34
pub workspace_path: String, - 35
pub cost_estimate_usd: Option<f64>, - 36
/// The command/script the approval gates (if applicable, e.g. for `bash`). - 37
pub original_command: String, - 38
/// Mutable in-place-edited copy of the command. - 39
pub edited_command: String, - 40
pub is_editing: bool, - 41
pub edit_cursor: usize, - 42
pub remember: RememberMode, - 43
} - 44
- 45
impl HilApprovalState { - 46
/// Build from a real `ApprovalRequested` agent event. - 47
pub fn from_approval_event( - 48
session_id: impl Into<String>, - 49
request_id: impl Into<String>, - 50
tool_name: impl Into<String>, - 51
args_json: impl Into<String>, - 52
reason: impl Into<String>, - 53
workspace_path: impl Into<String>, - 54
cost_estimate_usd: Option<f64>, - 55
) -> Self { - 56
let tool_name = tool_name.into(); - 57
let args_json = args_json.into(); - 58
let cmd = Self::command_from_args(&tool_name, &args_json); - 59
let cmd_owned = cmd.clone(); - 60
Self { - 61
request_id: request_id.into(), - 62
session_id: session_id.into(), - 63
tool_name, - 64
args_json, - 65
risk_reason: reason.into(), - 66
workspace_path: workspace_path.into(), - 67
cost_estimate_usd, - 68
original_command: cmd_owned.clone(), - 69
edited_command: cmd_owned, - 70
is_editing: false, - 71
edit_cursor: cmd.len(), - 72
remember: RememberMode::No, - 73
} - 74
} - 75
- 76
/// Extract a human-readable command string from tool arguments. - 77
/// For `bash`-type tools this is the script; for others it is a - 78
/// rendered summary of the args JSON. - 79
fn command_from_args(tool_name: &str, args_json: &str) -> String { - 80
if tool_name == "bash" || tool_name == "shell" { - 81
if let Ok(val) = serde_json::from_str::<Value>(args_json) - 82
&& let Some(code) = val.get("command").and_then(|v| v.as_str()) - 83
{ - 84
return code.to_string(); - 85
} - 86
args_json.to_string() - 87
} else { - 88
// For non-bash tools, render the args as a compact summary. - 89
if let Ok(val) = serde_json::from_str::<Value>(args_json) { - 90
serde_json::to_string(&val).unwrap_or_else(|_| args_json.to_string()) - 91
} else { - 92
args_json.to_string() - 93
} - 94
} - 95
} - 96
- 97
pub fn start_editing(&mut self) { - 98
self.is_editing = true; - 99
self.edit_cursor = self.edited_command.len(); - 100
} - 101
- 102
pub fn cancel_editing(&mut self) { - 103
self.is_editing = false; - 104
self.edited_command = self.original_command.clone(); - 105
} - 106
- 107
pub fn toggle_remember(&mut self) { - 108
self.remember = match self.remember { - 109
RememberMode::No => RememberMode::Yes, - 110
RememberMode::Yes => RememberMode::No, - 111
}; - 112
} - 113
- 114
pub fn insert_char(&mut self, c: char) { - 115
if self.is_editing { - 116
if self.edit_cursor >= self.edited_command.len() { - 117
self.edited_command.push(c); - 118
} else { - 119
self.edited_command.insert(self.edit_cursor, c); - 120
} - 121
self.edit_cursor += c.len_utf8(); - 122
} - 123
} - 124
- 125
pub fn delete_backspace(&mut self) { - 126
if self.is_editing && self.edit_cursor > 0 && !self.edited_command.is_empty() { - 127
let mut prev = self.edit_cursor - 1; - 128
while prev > 0 && !self.edited_command.is_char_boundary(prev) { - 129
prev -= 1; - 130
} - 131
self.edited_command.remove(prev); - 132
self.edit_cursor = prev; - 133
} - 134
} - 135
- 136
pub fn finish_approval(&self) -> HilOutcome { - 137
let modified = if self.edited_command != self.original_command { - 138
Some(self.edited_command.clone()) - 139
} else { - 140
None - 141
}; - 142
HilOutcome::ApproveOnce { - 143
modified_command: modified, - 144
} - 145
} - 146
- 147
/// Whether the edited command differs from the original. - 148
pub fn has_modification(&self) -> bool { - 149
self.edited_command != self.original_command - 150
} - 151
} - 152
- 153
/// Helper to extract an `ApprovalRequested` event from an `AgentEvent` JSON - 154
/// value (as received over the SSE event stream). Returns `None` if the - 155
/// event is not an approval request or is malformed. - 156
pub fn extract_approval_request(event_json: &serde_json::Value) -> Option<RealApprovalRequest> { - 157
// The server emits AgentEvent::ApprovalRequested as: - 158
// {"ApprovalRequested": {"id": "...", "tool": "...", "args_json": "...", "reason": "..."}} - 159
let approval = event_json.get("ApprovalRequested")?; - 160
Some(RealApprovalRequest { - 161
request_id: approval - 162
.get("id") - 163
.and_then(|v| v.as_str()) - 164
.map(String::from)?, - 165
tool: approval - 166
.get("tool") - 167
.and_then(|v| v.as_str()) - 168
.map(String::from)?, - 169
args_json: approval - 170
.get("args_json") - 171
.and_then(|v| v.as_str()) - 172
.map(String::from) - 173
.unwrap_or_default(), - 174
reason: approval - 175
.get("reason") - 176
.and_then(|v| v.as_str()) - 177
.map(String::from) - 178
.unwrap_or_default(), - 179
}) - 180
} - 181
- 182
/// A real approval request extracted from the agent event stream. - 183
#[derive(Debug, Clone)] - 184
pub struct RealApprovalRequest { - 185
pub request_id: String, - 186
pub tool: String, - 187
pub args_json: String, - 188
pub reason: String, - 189
} - 190
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.