Skip to content

Commit 3398610

Browse files
authored
feat: route clarification replies over stdio (A6) (#360)
1 parent 6af8ea5 commit 3398610

3 files changed

Lines changed: 51 additions & 1 deletion

File tree

src-tauri/crates/altai-cli/src/serve/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub async fn run(workspace: WorkspacePaths) -> Result<(), String> {
2727
event_sink.clone() as Arc<dyn altai_agent_service::AgentEventSink>,
2828
run_coordinator.clone(),
2929
));
30-
let service = Arc::new(AgentService::with_coordinator(host, run_coordinator.clone()));
30+
let service = Arc::new(AgentService::with_coordinator(host.clone(), run_coordinator.clone()));
3131

3232
let mut stdin = tokio::io::stdin();
3333
let mut decoder = FrameDecoder::new(FrameLimits::default());
@@ -96,6 +96,7 @@ pub async fn run(workspace: WorkspacePaths) -> Result<(), String> {
9696
"run/cancel",
9797
"run/steer",
9898
"run/replay",
99+
"clarification/respond",
99100
"context/compact",
100101
"checkpoints/list",
101102
"checkpoints/restore",
@@ -434,6 +435,21 @@ pub async fn run(workspace: WorkspacePaths) -> Result<(), String> {
434435
"run/replay" if initialized => {
435436
handle_run_replay(&workspace, &writer, id, params).await?;
436437
}
438+
"clarification/respond" if initialized => {
439+
let params = params
440+
.and_then(|value| value.as_object().cloned())
441+
.unwrap_or_default();
442+
let chat_id = params.get("chat_id").and_then(Value::as_str).unwrap_or("");
443+
let text = params.get("text").and_then(Value::as_str).unwrap_or("");
444+
if chat_id.trim().is_empty() || chat_id.len() > 256 || text.trim().is_empty() || text.len() > 16_384 {
445+
respond(&writer, id, None, Some(error_value(-32602, "invalid_clarification_response"))).await?;
446+
continue;
447+
}
448+
match host.deliver_clarification_reply(chat_id, text.to_string()).await {
449+
Ok(()) => respond(&writer, id, Some(json!({"accepted": true})), None).await?,
450+
Err(error) => respond(&writer, id, None, Some(error_value(-32002, &error))).await?,
451+
}
452+
}
437453
"context/compact" if initialized => {
438454
let params = params
439455
.and_then(|value| value.as_object().cloned())

src-tauri/crates/altai-cli/src/stdio_host.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ impl StdioHost {
6565
}
6666
}
6767

68+
/// Deliver a reply only when this stdio chat has a live clarification wait.
69+
/// The hub removes the pending slot before delivery, making repeated replies
70+
/// fail rather than resuming the same tool twice.
71+
pub async fn deliver_clarification_reply(
72+
&self,
73+
chat_id: &str,
74+
text: String,
75+
) -> Result<(), String> {
76+
let workspace_root = self.workspace.root.to_string_lossy().to_string();
77+
let services = self.workspace_bundle_inner(&workspace_root).await?;
78+
let session_key = isanagent::bus::clarification_session_key("stdio", chat_id, None);
79+
if services
80+
.clarification_hub
81+
.try_deliver_reply(&session_key, text)
82+
{
83+
Ok(())
84+
} else {
85+
Err("clarification_not_pending".to_string())
86+
}
87+
}
88+
6889
async fn workspace_bundle_inner(
6990
&self,
7091
workspace_root: &str,

src-tauri/crates/altai-cli/tests/serve_stdio.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ fn config_update_persists_a_non_secret_model_setting() {
143143
let _stderr = process.shutdown();
144144
}
145145

146+
#[test]
147+
fn clarification_response_rejects_a_non_pending_ticket() {
148+
let workspace = tempfile::tempdir().expect("workspace");
149+
let mut process = ServeProcess::spawn(workspace.path(), false);
150+
process.frame(initialize(json!(1)));
151+
assert_eq!(process.next()["id"], 1);
152+
process.frame(json!({"jsonrpc":"2.0","id":2,"method":"clarification/respond","params":{"chat_id":"chat-test","text":"yes"}}));
153+
assert_eq!(process.next()["error"]["message"], "clarification_not_pending");
154+
process.frame(json!({"jsonrpc":"2.0","id":3,"method":"shutdown"}));
155+
assert_eq!(process.next()["id"], 3);
156+
let _stderr = process.shutdown();
157+
}
158+
146159
#[test]
147160
fn compiled_stdio_handles_split_and_multiple_frames_with_ordered_terminal_stream() {
148161
let workspace = tempfile::tempdir().expect("workspace");

0 commit comments

Comments
 (0)